Package qubx :: Module GTK
[hide private]
[frames] | no frames]

Source Code for Module qubx.GTK

   1  """General-purpose UI components. 
   2   
   3  Copyright 2007-2014 Research Foundation State University of New York  
   4  This file is part of QUB Express.                                           
   5   
   6  QUB Express is free software; you can redistribute it and/or modify           
   7  it under the terms of the GNU General Public License as published by  
   8  the Free Software Foundation, either version 3 of the License, or     
   9  (at your option) any later version.                                   
  10   
  11  QUB Express is distributed in the hope that it will be useful,                
  12  but WITHOUT ANY WARRANTY; without even the implied warranty of        
  13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
  14  GNU General Public License for more details.                          
  15   
  16  You should have received a copy of the GNU General Public License,    
  17  named LICENSE.txt, in the QUB Express program directory.  If not, see         
  18  <http://www.gnu.org/licenses/>.                                       
  19   
  20  """ 
  21   
  22  import pygtk 
  23  pygtk.require('2.0') 
  24  import gtk 
  25  from gtk import gdk 
  26  from gtk import keysyms 
  27  import gobject 
  28  import pango 
  29  import threading 
  30  import traceback 
  31  import re 
  32  import sys 
  33  import datetime 
  34  import thread 
  35  from math import * 
  36  from qubx.util_types import * 
  37  from qubx.accept import * 
  38   
  39   
40 -class AskOnceDialog(gtk.Dialog):
41 """ 42 Shows a message, some buttons, and a checkbox "[x] don't show this message again." 43 """
44 - def __init__(self, title='', parent=None, message='', buttons=('OK', gtk.RESPONSE_ACCEPT), 45 default_response=gtk.RESPONSE_ACCEPT, dont_show_again=True, 46 dont_show_caption="Don't show this message again"):
47 """ 48 @param title: title of the dialog 49 @param parent: parent gtk.Window 50 @param message: some text 51 @param buttons: list of (string_label, response) 52 @param default_response: see gtk.Dialog.set_default_response 53 @param dont_show_again: initial state of check box 54 """ 55 gtk.Dialog.__init__(self, title, parent or get_active_window(), gtk.DIALOG_MODAL, buttons=None) 56 pack_label(message, self.get_content_area(), expand=True) 57 action_v = pack_item(gtk.VBox(), self.get_content_area(), expand=True) 58 vh = pack_item(gtk.HBox(), action_v) 59 for id, resp in reversed(buttons): 60 pack_button(id, vh, bind(self.response, resp), at_end=True) 61 self.set_default_response(default_response) 62 63 vh = pack_item(gtk.HBox(), action_v) 64 self.chkDontShow = pack_check(dont_show_caption, vh, active=dont_show_again)
65 - def run(self):
66 response = gtk.Dialog.run(self) 67 self.hide() 68 return response, self.chkDontShow.get_active()
69
70 -class InputDialog(gtk.Dialog):
71 """ 72 Prompts the user to write or edit a string. 73 """
74 - def __init__(self, title='', parent=None, message='', text='', buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, 75 gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT) ):
76 """ 77 @param title: title of the dialog 78 @param parent: parent gtk.Window, for centering 79 @param message: label to the left of the string 80 @param text: initial contents of the string 81 @param buttons: same as the gtk.Dialog constructor argument 82 """ 83 gtk.Dialog.__init__(self, title, parent or get_active_window(), gtk.DIALOG_MODAL, buttons=buttons) 84 self.__ref = Reffer() 85 self.set_default_response(gtk.RESPONSE_ACCEPT) 86 hbox = pack_item(gtk.HBox(), self.vbox, expand=True) 87 pack_label(message, hbox, expand=True) 88 self.entry = pack_item(NumEntry(text), hbox, expand=True) 89 self.entry.OnChange += self.__ref(self.__onChange) # enter -> ok
90 - def __onChange(self, entry, val):
91 entry.get_toplevel().child_focus(gtk.DIR_TAB_FORWARD)
92 - def run(self):
93 response = gtk.Dialog.run(self) 94 self.text = self.entry.value 95 return response
96 97 98
99 -class NumEntryDialog(gtk.Dialog):
100 """ 101 Prompts the user to write or edit a string. 102 """
103 - def __init__(self, title='', parent=None, message='', value='', accept=None, format=str, 104 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 105 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT) ):
106 """ 107 @param title: title of the dialog 108 @param parent: parent gtk.Window, for centering 109 @param message: label to the left of the string 110 @param value: initial value 111 @param accept: f(string) returns value or raises Exception 112 see qubx.accept for stock examples 113 @param format: either a %-style format string 114 or f(value) -> string 115 @param buttons: same as the gtk.Dialog constructor argument 116 """ 117 gtk.Dialog.__init__(self, title, parent or get_active_window(), gtk.DIALOG_MODAL, buttons=buttons) 118 self.__ref = Reffer() 119 self.set_default_response(gtk.RESPONSE_ACCEPT) 120 hbox = pack_item(gtk.HBox(), self.vbox, expand=True) 121 lbl = pack_label(message, hbox, expand=True) 122 self.entry = pack_item(NumEntry(value, accept, format), hbox, expand=True) 123 self.entry.OnChange += self.__ref(self.__onChange) # enter -> ok
124 #self.entry.OnReset += self.__ref(self.__onReset) # esc -> cancel # disabled because it can dismiss on focus events
125 - def __onChange(self, entry, val):
126 self.response(gtk.RESPONSE_ACCEPT)
127 - def __onReset(self, entry, val):
128 self.response(gtk.RESPONSE_REJECT)
129 - def run(self):
130 response = gtk.Dialog.run(self) 131 self.value = self.entry.value 132 return response
133 134 135
136 -class NumEntriesDialog(gtk.Dialog):
137 """ 138 Prompts the user to write or edit some values. 139 140 @ivar values: dict[key] -> value, after run() 141 """
142 - def __init__(self, title='', items=[], parent=None, 143 buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT) ):
144 """ 145 @param title: title of the dialog 146 @param parent: parent gtk.Window, for centering 147 @param items: list of tuple (key, caption, value, accept, format) 148 @param buttons: same as the gtk.Dialog constructor argument 149 """ 150 gtk.Dialog.__init__(self, title, parent or get_active_window(), gtk.DIALOG_MODAL, buttons=buttons) 151 self.__ref = Reffer() 152 self.set_default_response(gtk.RESPONSE_ACCEPT) 153 self.entries = {} 154 for item in items: 155 self.add_item(*item)
156 - def add_item(self, key, caption, value, accept=acceptFloat, format='%.5g'):
157 if (value is None) and (accept is None) and (format is None): 158 pack_label(caption, self.vbox, expand=True) 159 self.entries[key] = Anon(value=None) 160 else: 161 hbox = pack_item(gtk.HBox(True), self.vbox) 162 lbl = pack_label(caption, hbox, expand=True) 163 entry = pack_item(NumEntry(value, accept, format), hbox, expand=True) 164 entry.OnChange += self.__ref(bind_with_args_before(self.__onChange, key)) 165 self.entries[key] = entry
166 - def __onChange(self, entry, val, key):
167 entry.get_toplevel().child_focus(gtk.DIR_TAB_FORWARD)
168 - def run(self):
169 response = gtk.Dialog.run(self) 170 self.values = {} 171 for key in self.entries: 172 self.values[key] = self.entries[key].value 173 return response
174 175 176 177 MAX_EVENTS_PER_WAIT = 20 178
179 -class BusyDialog(gtk.Dialog):
180 """ 181 Modal dialog with progress bar and stop button; supervises a long-running computation. 182 """
183 - def __init__(self, title, func, finish_func=lambda:None, 184 stopCoords=None, parent=None, maxEventsPerUpdate=MAX_EVENTS_PER_WAIT):
185 """ 186 Inside your long-task callback: 187 188 >>> func( update ) 189 190 You should the update function regularly to give progress and find out if stopped: 191 192 >>> update( fraction ) -> bool should_continue 193 194 @param title: text for the title bar 195 @param func: long-running computation func(update) 196 should periodically call update( fraction ) 197 and stop immediately if update returns False 198 @param finish_func called on successful completion (no args) 199 @param stopCoords: screen coordinates of the Stop button, for repositioning 200 @param parent: parent gtk.Window 201 @param maxEventsPerUpdate: process up to this many gui events each time you call update 202 """ 203 gtk.Dialog.__init__(self, title, parent or get_active_window(), gtk.DIALOG_MODAL) 204 self.prog = pack_item(gtk.ProgressBar(), self.vbox, expand=True) 205 self.btnStop = pack_button('Stop', self.action_area, self.onStop) 206 self.func = func 207 self.finish_func = finish_func 208 self.maxEvents = maxEventsPerUpdate 209 self._stopped = False 210 self.stopCoords = stopCoords 211 self.realize()
212 - def run(self):
213 """Shows the dialog and runs func.""" 214 self.show() 215 gobject.idle_add(self.start) 216 result = gtk.Dialog.run(self) 217 if self.exc: 218 raise self.exc[0], self.exc[1], self.exc[2] 219 if result == gtk.RESPONSE_OK: 220 self.finish_func() 221 return result
222 - def start(self):
223 if not (self.stopCoords is None): 224 x, y = self.stopCoords 225 xb, yb = self.btnStop.translate_coordinates(self, 0, 0) 226 self.move(x-xb, y-yb) 227 # print x,xb, y,yb 228 try: 229 self.exc = None 230 self.func(self.update) 231 except: 232 self.exc = sys.exc_info() 233 if self._stopped: 234 self.response(gtk.RESPONSE_CANCEL) 235 else: 236 self.response(gtk.RESPONSE_OK)
237 - def onStop(self, widget):
238 self._stopped = True
239 - def update(self, fraction):
240 self.prog.set_fraction(max(0.0, min(1.0, fraction))) 241 for i in xrange(self.maxEvents): 242 if gtk.events_pending(): 243 gtk.main_iteration(False) 244 else: 245 break 246 return not self._stopped
247 248 HARMLESS_KEYS = [keysyms.Left, keysyms.Right, keysyms.Up, keysyms.Down, keysyms.Home, keysyms.End, keysyms.Insert] 249
250 -class NumEntry(gtk.Entry):
251 """ 252 Originally intended for numbers, this gtk.Entry can parse, validate and display any suitable data type. 253 254 @ivar accept: as above 255 @ivar value: the last accepted value 256 @ivar text: the last accepted string 257 @ivar cGood: gdk.Color of accepted text 258 @ivar cDiff: gdk.Color of not-yet-accepted text 259 @ivar good: whether the text is accepted (what color to use) 260 261 @ivar OnEdit: L{WeakEvent}(NumEntry, txt) user has typed, text is now txt 262 @ivar OnChange: L{WeakEvent}(NumEntry, val) text accepted; new value is val 263 @ivar OnExit: L{WeakEvent}(NumEntry) focus-out 264 @ivar OnReset: L{WeakEvent}(NumEntry, val) 265 @ivar OnReject: L{WeakEvent}(NumEntry, txt, exc) txt not accepted due to exception exc 266 """ 267 268 __explore_featured = ['accept', 'format', 'value', 'text', 'cGood', 'cDiff', 'good', 269 'OnEdit', 'OnChange', 'OnExit', 'OnReset', 'OnReject', 270 'onParse'] 271
272 - def __init__(self, value, accept=None, format='%s', width_chars=None):
273 """ 274 @param value: initial value 275 @param accept: f(string) returns value or raises Exception 276 see qubx.accept for stock examples 277 default is str 278 @param format: either a %-style format string 279 or f(value) -> string 280 @param width_chars: shortcut to self.set_width_chars(width_chars) 281 """ 282 gtk.Entry.__init__(self) 283 # self.set_size_request(80, -1) 284 self.format = acceptFormat(format) 285 self.connect("key_press_event", self.onKey) 286 self.connect("focus_out_event", self.onExit) 287 self.OnEdit = WeakEvent() # OnEdit(NumEntry, txt) 288 self.OnChange = WeakEvent() # OnChange(NumEntry, val) 289 self.OnExit = WeakEvent() # OnExit(NumEntry) 290 self.OnReject = WeakEvent() # OnReject(NumEntry, txt, exception) 291 self.OnReset = WeakEvent() # OnReset(NumEntry, val) 292 self._cGood = gdk.color_parse("#000000") 293 self._cDiff = gdk.color_parse("#ff0000") 294 self._good = True 295 self._accept = accept or (isinstance(value, str) and str) or (lambda s: type(value)(eval(s))) 296 self.value = value 297 if width_chars: 298 self.set_width_chars(width_chars)
299 - def setAccept(self, accept):
300 self._accept = accept 301 self.onParse()
302 accept = property(lambda s: s._accept, setAccept)
303 - def setValue(self, value, repr=None, good=True, parse=False):
304 self._value = value 305 if repr is None: 306 self._str = self.format(value) 307 else: 308 self._str = repr 309 self.good = good 310 self.set_text(self._str) 311 if parse: 312 self.onParse()
313 value = property(lambda s: s._value, setValue) 314 text = property(lambda s: s._str)
315 - def setGoodColor(self, c):
316 self._cGood = c 317 self._good = not self._good # force update 318 self.good = not self.good
319 cGood = property(lambda s: s._cGood, setGoodColor)
320 - def setDiffColor(self, c):
321 self._cDiff = c 322 self._good = not self._good # force update 323 self.good = not self.good
324 cDiff = property(lambda s: s._cDiff, setDiffColor)
325 - def setGood(self, good):
326 if self._good != good: 327 self._good = good 328 self.modify_text(gtk.STATE_NORMAL, self._good and self._cGood or self._cDiff) 329 self.modify_text(gtk.STATE_SELECTED, self._good and self._cGood or self._cDiff)
330 good = property(lambda s: s._good, setGood)
331 - def onKey(self, txt, event):
332 if event.keyval == keysyms.Escape: 333 self.set_text(self._str) 334 self.select_region(0, -1) 335 self.good = True 336 self.OnReset(self, self.value) 337 elif event.keyval == keysyms.Return: 338 if self.get_text() == self._str: 339 self.OnChange(self, self.value) 340 self.good = True 341 else: 342 self.onParse() 343 elif (not (event.keyval in HARMLESS_KEYS)): 344 self.OnEdit(self, self.get_text()) 345 self.good = False
346 - def onExit(self, txt, event):
347 self.onParse() 348 self.OnExit(self)
349 - def onParse(self, force=False):
350 s = self.get_text() 351 if (not force) and (s == self._str): 352 self.good = True 353 self.OnReset(self, self.value) 354 else: 355 try: 356 self.setValue(self.accept(s), s) 357 self.OnChange(self, self.value) 358 self.good = True 359 self.select_region(0, -1) 360 except Exception, e: 361 self.good = False 362 self.OnReject(self, s, e)
363 # self.set_text(self._str) # self.format(self.value)) 364
365 -class CheckListEntryItem(Anon):
366 - def __init__(self, label, active):
368
369 -class CheckListEntry(gtk.Entry):
370 """ 371 Text widget showing state of a list of boolean items; clicking shows a popup with checkable items. 372 373 @ivar items: list of L{CheckListEntryItem} 374 @ivar menu_items: list of additional (caption, func(item)) for the popup e.g. ("Custom...", on_custom) 375 @ivar OnToggle: L{WeakEvent}(CheckListEntry, index, active) 376 """ 377 378 __explore_featured = ['items', 'menu_items', 'OnToggle', 'append', 'insert', 'remove', 'clear', 'get_active', 'set_active', 'update'] 379
380 - def __init__(self):
381 gtk.Entry.__init__(self) 382 self.OnToggle = WeakEvent() 383 self.set_editable(False) 384 self.connect('button_press_event', self.__onMouseDown) 385 self.items = [] 386 self.menu_items = []
387 - def append(self, label, active):
388 self.insert(len(self.items), label, active)
389 - def insert(self, index, label, active):
392 - def remove(self, index):
393 del self.items[index] 394 self.update()
395 - def clear(self):
396 self.items[:] = [] 397 self.update()
398 - def get_active(self, index):
399 return self.items[index].active
400 - def set_active(self, index, active):
401 self.items[index].active = active 402 self.update()
403 - def update(self):
404 self.set_text('; '.join(item.label for item in self.items if item.active))
405 - def __onMouseDown(self, selfy, event):
406 menu = gtk.Menu() 407 for index, item in enumerate(self.items): 408 mi = build_menuitem(item.label, sensitive=True, item_class=gtk.CheckMenuItem) 409 mi.set_active(item.active) 410 mi.connect('toggled', self.__onToggleItem, index) 411 menu.append(mi) 412 for caption, on_click in self.menu_items: 413 menu.append(build_menuitem(caption, on_click)) 414 menu.popup(None, None, None, 0, event.time)
415 - def __onToggleItem(self, mi, index):
416 active = not self.items[index].active 417 self.set_active(index, active) 418 self.OnToggle(self, index, active)
419
420 -class CustomCheckListEntry(CheckListEntry):
421 """ 422 Text widget showing state of a list of boolean items; clicking shows a popup with checkable items; 423 "Custom..." bypasses the checkboxes. 424 425 @ivar OnChange: L{WeakEvent}(CustomCheckListEntry, value_str) -- checks and custom too 426 """ 427 428 __explore_featured = ['OnChange', 'edit_custom', 'custom', 'custom_val'] 429
430 - def __init__(self, edit_custom=lambda x: x, menu_caption='Custom...'):
431 """@param edit_custom: func(expr) -> expr; return None for no-op e.g. on click cancel""" 432 CheckListEntry.__init__(self) 433 self.__ref = Reffer() 434 self.OnChange = WeakEvent() # (self, str) 435 self.edit_custom = edit_custom 436 self.menu_items.append((menu_caption, self.__onClickCustom)) 437 self.__custom = False 438 self.__custom_val = ""
439 custom = property(lambda self: self.__custom, doc="if it's True, ignore 'active' and use custom_val") 440 custom_val = property(lambda self: self.get_text(), lambda self, x: self.set_custom_val(x))
441 - def __onClickCustom(self, item):
442 new_val = self.edit_custom(self.get_text()) 443 if not (new_val is None): 444 self.custom_val = new_val
445 - def set_custom_val(self, x):
446 self.__custom = True 447 self.__custom_val = x 448 self.set_text(x) 449 self.OnChange(self, x)
450 - def set_active(self, index, active):
451 self.__custom = False 452 CheckListEntry.set_active(self, index, active) 453 self.OnChange(self, self.get_text())
454 - def update(self):
455 if not self.__custom: 456 self.set_text('; '.join(item.label for item in self.items if item.active))
457 458
459 -class V2Scale(gtk.Frame):
460 """ 461 Widget with two logarithmic sliders: coarse and fine. 462 463 @ivar octaves: coarse ranges from 1/(2^octaves) to 2^octaves 464 @ivar divs: number of sub-divisions per octave 465 @ivar zoom: 466 467 @ivar onScale: L{WeakEvent}(V2Scale, factor) dragging slider, now at factor 468 @ivar onSnap: L{WeakEvent}(V2Scale) released slider, it pops back to center 469 """ 470 471 __explore_featured = ['octaves', 'divs', 'zoom', 'onScale', 'onSnap', 'maj', 'majs', 'min', 'mins', 'snapState', 'sliding', 'reverse'] 472
473 - def __init__(self, octaves=4, divs=8, minor_zoom=8):
474 """ 475 @param octaves: coarse ranges from 1/(2^octaves) to 2^octaves 476 @param divs: number of sub-divisions per octave 477 @param minor_zoom: fine range is 1/minor_zoom of coarse range 478 """ 479 gtk.Frame.__init__(self) 480 self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) 481 self.set_size_request(-1, 50) 482 self.octs = octaves 483 self.divs = divs 484 self.zoom = minor_zoom 485 self.onScale = WeakEvent() # onScale(V2Scale, factor) 486 self.onSnap = WeakEvent() # onSnap(V2Scale) 487 # emit 2^(value/(divs*zoom)) 488 maxval = octaves * divs 489 h = gtk.HBox(True, 0) 490 self.maj = gtk.Adjustment( value=0, lower=-maxval, upper=maxval, step_incr=1, page_incr=divs, page_size=1 ) 491 self.maj.connect("value_changed", self.onSlide, 1) 492 self.majs = gtk.VScale(self.maj) 493 self.majs.connect("button_release_event", self.onEndSlide, self.maj) 494 self.majs.set_draw_value(False) 495 h.pack_start(self.majs, True, True) 496 self.majs.show() 497 self.min = gtk.Adjustment( value=0, lower=-maxval, upper=maxval, step_incr=1, page_incr=divs, page_size=1 ) 498 self.min.connect("value_changed", self.onSlide, minor_zoom) 499 self.mins = gtk.VScale(self.min) 500 self.mins.connect("button_release_event", self.onEndSlide, self.min) 501 self.mins.set_draw_value(False) 502 h.pack_start(self.mins, True, True) 503 self.mins.show() 504 self.snapState = 0 505 self.sliding = False 506 self.add(h) 507 h.show()
508 # we want it to snap back to 0 when released, emitting no further events, 509 # so we have to ignore some inconvenient events: 510 # snapState: 0: normal (sliding or not) 511 # 1: button-up (ignoring our setting to 0) 512 # 2: overriding bogus setting from mouse-up 513 # 3: ignoring our override 514 # TODO: this style disables pageup/dn single-clicks
515 - def onSlide(self, adj, zoom):
516 if self.snapState == 0: 517 self.sliding = True 518 self.onScale(self, pow(2.0, - adj.value / (self.divs * zoom))) 519 elif self.snapState == 1: 520 self.snapState = 2 521 elif self.snapState == 2: 522 adj.value = 0 523 self.snapState = 3 524 else: 525 self.onSnap(self) 526 self.snapState = 0 527 self.sliding = False
528 - def onEndSlide(self, slide, evt, adj):
529 self.snapState = 1 530 adj.set_value(0)
531 - def reverse(self, factor):
532 """If factor is modifying a negative number, you want up to mean abs(smaller).""" 533 if factor == 0.0: return 0.0 534 power = log(factor) / log(2.0) 535 return pow(2.0, - power)
536
537 -class NumSliders(gtk.VBox):
538 """ 539 Widget containing V2Scale and NumEntry, for kbd and log-slider editing of a number. 540 541 @ivar txt: the NumEntry 542 @ivar slide: the V2Scale 543 @ivar center: value corresponding to the central slider position; 544 center == txt.value except when dragging a slider 545 @ivar OnChange: L{WeakEvent}(val) 546 547 """ 548 549 __explore_featured = ['slide', 'center', 'txt', 'OnChange', 'set_value'] 550
551 - def __init__(self, format="%s"):
552 """@param format: see L{NumEntry}""" 553 gtk.VBox.__init__(self, False, 0) 554 self.slide = V2Scale() 555 self.hOnScale = self.onScale 556 self.slide.onScale += self.hOnScale,"onScale" 557 self.hOnSnap = self.onSnap 558 self.slide.onSnap += self.hOnSnap,"onSnap" 559 self.pack_start(self.slide, True, True) 560 self.slide.show() 561 self.center = 0.0 562 self.txt = NumEntry(self.center, format=format) 563 self.txt.set_width_chars(7) 564 self.hOnNumChange = self.onNumChange 565 self.txt.OnChange += self.hOnNumChange,"onNumChange" 566 self.pack_start(self.txt, False, True) 567 self.txt.show() 568 self.OnChange = WeakEvent() # (value)
569 - def set_value(self, value):
570 if not self.slide.sliding: 571 self.txt.value = value 572 self.center = value
573 # self.OnChange(value)
574 - def onScale(self, scale, factor):
575 if self.center == 0.0: 576 self.txt.value = factor - 1.0 577 elif self.center < 0: 578 self.txt.value = scale.reverse(factor) * self.center 579 else: 580 self.txt.value = factor * self.center 581 self.OnChange(self.txt.value)
582 - def onSnap(self, scale):
583 self.center = self.txt.value
584 - def onNumChange(self, txt, value):
585 self.center = value 586 self.OnChange(value)
587 588
589 -class Pow10Slider(gtk.HScale):
590 """ 591 Horizontal logarithmish slider with steps at [s * top * 10**(p - powers) for s in steps, for p in range(powers)] + [top]. 592 593 For example, steps=[1, 5], powers=3, top=10: 594 - values = [.01, .05, .1, .5, 1, 5, 10] 595 596 @ivar steps: list of stops to use at each power of 10 597 @ivar powers: number of powers of 10, in [..., -3, -2, -1] to list 598 @ivar top: top of the scale; on the order of the largest level listed; automatically rounded up to power of 10 599 @ivar values: list of numbers: all the slider stops 600 @ivar value: numerical value, which need not be in the list of values 601 @ivar OnChange: L{WeakEvent}(Pow10Slider, value) when the slider is moved 602 """ 603 604 __explore_featured = ['steps', 'powers', 'top', 'values', 'value', 'OnChange'] 605
606 - def __init__(self, steps=[1.0, 2.0, 3.0, 5.0, 8.0], powers=4, top=1.0):
607 n = len(steps) * powers + 1 608 self.__adj = gtk.Adjustment(value=n-1, lower=0, upper=n, step_incr=1, page_incr=len(steps), page_size=1) 609 self.__adj.connect('value_changed', self.__onSlide) 610 gtk.HScale.__init__(self, self.__adj) 611 self.set_draw_value(False) 612 self.__steps = steps 613 self.__powers = powers 614 self.__top = 10**int(ceil(log10(top))) 615 self.__value = top 616 self.__approximating = False 617 self.__setup_values() 618 self.OnChange = WeakEvent() # (Pow10Slider, value)
619 steps = property(lambda self: self.__steps, lambda self, x: self.set_steps(x)) 620 powers = property(lambda self: self.__powers, lambda self, x: self.set_powers(x)) 621 top = property(lambda self: self.__top, lambda self, x: self.set_top(x)) 622 value = property(lambda self: self.__value, lambda self, x: self.set_value(x))
623 - def set_steps(self, x):
624 self.__steps = steps 625 self.__setup()
626 - def set_powers(self, x):
627 self.__powers = x 628 self.__setup()
629 - def set_top(self, x):
630 top = 10**int(ceil(log10(x))) 631 if self.__top == top: return 632 self.__top = top 633 self.__setup_values() 634 self.__update_slider()
635 - def set_value(self, x):
636 if self.__value == x: return 637 self.__value = x 638 self.__update_slider()
639 - def __setup_values(self):
640 nstep = len(self.__steps) 641 self.values = [self.__top * self.__steps[i % nstep] * 10**(i/nstep - self.__powers) for i in xrange(nstep*self.__powers+1)]
642 - def __setup(self):
643 n = len(self.__steps) * self.__powers + 1 644 self.__adj.upper = n 645 self.__adj.page_incr=len(self.__steps) 646 self.__adj.emit('changed') 647 self.__setup_values() 648 self.__update_slider()
649 - def __update_slider(self):
650 self.__approximating = True 651 self.__adj.set_value(min(((self.__value-x)**2, i) for i,x in enumerate(self.values))[1]) 652 self.__approximating = False
653 - def __onSlide(self, adj):
654 if not self.__approximating: 655 nstep = len(self.__steps) 656 self.value = self.values[int(adj.value)] 657 self.OnChange(self, self.value)
658
659 -class DateBox(gtk.HBox):
660 """ 661 Widget for entering/editing legitimate datetime.datetime values. 662 663 @ivar date: datetime.datetime 664 @ivar OnChange: L{WeakEvent}(DateBox, date) 665 666 """ 667 668 __explore_featured = ['day', 'month', 'year', 'date', 'OnChange'] 669
670 - def __init__(self, *args, **kw):
671 gtk.HBox.__init__(self, *args, **kw) 672 self.day = NumEntry(1, self._acceptDay, "%d") 673 self.hOnChangeDay = self._onChangeDay 674 self.day.OnChange += self.hOnChangeDay 675 self.day.set_width_chars(2) 676 self.pack_start(self.day, False, False) 677 self.day.show() 678 self.month = gtk.combo_box_new_text() 679 for m in xrange(1, 13): 680 self.month.append_text(datetime.datetime(1999, m, 1).strftime("%b")) 681 self.month.connect('changed', self._onChangeMonth) 682 self.pack_start(self.month, False, False) 683 self.month.show() 684 self.year = NumEntry(1999, int, "%d") 685 self.hOnChangeYear = self._onChangeYear 686 self.year.OnChange += self.hOnChangeYear 687 self.year.set_width_chars(4) 688 self.pack_start(self.year, False, False) 689 self.year.show() 690 self.OnChange = WeakEvent() # (DateBox, datetime) 691 self.set_date(datetime.datetime.now())
692 - def set_date(self, date):
693 self._updating = True 694 self._date = date 695 self.day.setValue(date.day) 696 self.month.set_active(date.month-1) 697 self.year.setValue(date.year) 698 self._updating = False
699 date = property(lambda s: s._date, set_date)
700 - def _acceptDay(self, s):
701 self.date.replace(day=int(s)) 702 return int(s)
703 - def _onChangeDay(self, txt, val):
704 if self._updating: return 705 self.date = self.date.replace(day=val) 706 self.OnChange(self, self.date)
707 - def _onChangeMonth(self, mnu):
708 if self._updating: return 709 while True: # decrement day until it fits into new month 710 try: 711 self.date = self.date.replace(month=mnu.get_active()+1) 712 self.OnChange(self, self.date) 713 return 714 except: 715 self._updating = True 716 self.date = self.date.replace(day=self.date.day-1) 717 self.day.setValue(self.date.day) 718 self._updating = False
719 - def _onChangeYear(self, txt, val):
720 if self._updating: return 721 self.date = self.date.replace(year=val) 722 self.OnChange(self, self.date)
723 724
725 -class StaticComboList(gtk.EventBox):
726 """ 727 A menu whose choices never change. 728 729 @ivar active_i: index of the chosen item 730 @ivar active_text: text of the chosen item 731 732 @ivar OnChange: L{WeakEvent}(StaticComboList, active_text) when a different choice is made 733 """ 734 735 __explore_featured = ['active_i', 'active_text', 'OnChange', 'choices', 'indexOf', 'add_choice', 'remove_choice', 'choose'] 736
737 - def __init__(self, choices):
738 gtk.EventBox.__init__(self) 739 self.choices = [] 740 self._box = gtk.combo_box_new_text() 741 self.add(self._box) 742 self._box.show() 743 self.indexOf = {} 744 self._count = 0 745 for i, choice in enumerate(choices): 746 self.add_choice(choice) 747 if not self._count: 748 self._active_text = '' 749 self._active_i = -1 750 self._changed_handler = self._box.connect('changed', self.combo_changed) 751 self.OnChange = WeakEvent() # ComboBox, active-text
752 - def add_choice(self, choice):
753 self.choices.append(choice) 754 self._box.append_text(choice) 755 self.indexOf[choice] = self._count 756 self._count += 1 757 if self._count == 1: 758 self._box.set_active(0) 759 self._active_text = choice 760 self._active_i = 0
761 - def remove_choice(self, choice):
762 i = self.choices.index(choice) 763 if self._active_i == i: 764 if (i+1) < self._count: 765 self.active_i = i+1 766 else: 767 self._active_i = i-1 768 self.OnChange(self._box, self._active_text) 769 self.choices.remove(choice) 770 self._count -= 1 771 for j in xrange(i, self._count): 772 self.indexOf[self.choices[j]] = j 773 self._box.remove_text(i)
774 active_i = property(lambda s: s._active_i, lambda s, x: s.set_active_i(x)) 775 active_text = property(lambda s: s._active_text, lambda s, x: s.choose(x))
776 - def choose(self, txt):
777 """Selects the item with txt.""" 778 if self.indexOf.has_key(txt): 779 self.set_active_i(self.indexOf[txt])
780 - def set_active_i(self, x):
781 if 0 <= x < len(self.indexOf): 782 self._box.handler_block(self._changed_handler) 783 self._box.set_active(x) 784 self._active_i = x 785 self._active_text = self.choices[x] 786 self._box.handler_unblock(self._changed_handler) 787 else: 788 self._active_i = -1 789 self._active_text = ""
790 - def combo_changed(self, combo):
791 active_i = combo.get_active() 792 if active_i < 0: 793 combo.set_active(self._active_i) 794 else: 795 self._active_i = active_i 796 self._active_text = combo.get_active_text() 797 self.OnChange(combo, self._active_text)
798
799 -class DynamicComboBox(gtk.EventBox):
800 """ 801 A menu which is re-built each time it drops down. 802 803 @ivar active_i: index of the chosen item 804 @ivar active_text: text of the chosen item 805 806 @ivar OnPopulate: L{WeakEvent}(add) when dropping down; call add(item_text) for each desired item 807 @ivar OnChanged: L{WeakEvent}(DynamicComboBox, active_text) when a different choice is made 808 """ 809 810 __explore_featured = ['active_i', 'active_text', 'OnPopulate', 'OnChanged', 'indexOf', 'choose', 'repopulate'] 811
812 - def __init__(self):
813 gtk.EventBox.__init__(self) 814 self._box = gtk.combo_box_new_text() 815 self.add(self._box) 816 self._box.show() 817 self._changed_handler = self._box.connect('changed', self.combo_changed) 818 self.OnPopulate = WeakEvent() # add(item-text) 819 self.OnChanged = WeakEvent() # ComboBox, active-text 820 self._count = 0 821 self._active_text = '' 822 self._active_i = -1 823 self.indexOf = {} 824 self.connect('enter-notify-event', self.enter_notify_event)
825 active_i = property(lambda s: s._active_i) 826 active_text = property(lambda s: s._active_text, lambda s, x: s.choose(x))
827 - def _add(self, text):
828 self._box.append_text(text) 829 self._count += 1 830 if text == self._active_text: 831 self._box.set_active(self._count - 1) 832 self.indexOf[text] = self._count - 1
833 - def choose(self, txt):
834 """Selects the item with txt, adding it if necessary.""" 835 self._box.handler_block(self._changed_handler) 836 if not self.indexOf.has_key(txt): 837 self._add(txt) 838 self._active_i = self.indexOf[txt] 839 self._active_text = txt 840 self._box.set_active(self._active_i) 841 self._box.handler_unblock(self._changed_handler)
842 - def enter_notify_event(self, widget, event):
843 self.repopulate()
844 - def repopulate(self):
845 self._active_text = self._box.get_active_text() 846 self._box.handler_block(self._changed_handler) 847 for i in xrange(self._count): 848 self._box.remove_text(0) 849 self._count = 0 850 self.indexOf = {} 851 self.OnPopulate(self._add) 852 self._box.handler_unblock(self._changed_handler)
853 - def combo_changed(self, combo):
854 active_i = combo.get_active() 855 if active_i < 0: 856 combo.set_active(self._active_i) 857 else: 858 self._active_i = active_i 859 self._active_text = combo.get_active_text() 860 self.OnChanged(combo, self._active_text)
861
862 -class SuggestiveComboBox(gtk.EventBox):
863 """ 864 Like L{NumEntry}, with drop-down list of suggestions. 865 866 Properties: 867 @ivar accept: as above 868 @ivar value: the last accepted value 869 @ivar text: the last accepted string 870 @ivar cGood: gdk.Color of accepted text 871 @ivar cDiff: gdk.Color of not-yet-accepted text 872 @ivar good: whether the text is accepted (what color to use) 873 874 @ivar OnEdit: L{WeakEvent}(combo, txt) user has typed, text is now txt 875 @ivar OnChange: L{WeakEvent}(combo, val) text accepted; new value is val 876 @ivar OnExit: L{WeakEvent}(combo) focus-out 877 @ivar OnReject: L{WeakEvent}(combo, txt, exc) txt not accepted due to exception exc 878 @ivar OnPopup: L{WeakEvent}(combo, suggest) showing menu, suggest('menu item') 879 @ivar OnSuggest: L{WeakEvent}(combo, item_text) 880 """ 881 882 __explore_featured = ['accept', 'format', 'value', 'text', 'cGood', 'cDiff', 'good', 883 'OnEdit', 'OnChange', 'OnExit', 'OnReject', 'OnPopup', 'OnSuggest', 884 'repopulate', 'onParse'] 885
886 - def __init__(self, value, accept=None, format='%s'):
887 """ 888 @param value: initial value 889 @param accept: f(string) returns value or raises Exception; 890 see qubx.accept for stock examples; 891 default is str 892 @param format: either a %-style format string 893 or f(value) -> string 894 """ 895 gtk.EventBox.__init__(self) 896 self._box = gtk.combo_box_entry_new_text() 897 self.add(self._box) 898 self._box.show() 899 self._changed_handler = self._box.connect('changed', self.combo_changed) 900 self._box.connect('notify::popup-shown', self._onPopup) 901 self._box.set_property('button-sensitivity', gtk.SENSITIVITY_ON) 902 self._entry = self._box.child 903 self.format = acceptFormat(format) 904 self._entry.connect("key_press_event", self.onKey) 905 self._entry.connect("focus_out_event", self.onExit) 906 self.OnEdit = WeakEvent() 907 self.OnChange = WeakEvent() 908 self.OnExit = WeakEvent() 909 self.OnReject = WeakEvent() 910 self.OnReset = WeakEvent() 911 self.OnPopup = WeakEvent() 912 self.OnSuggest = WeakEvent() 913 self._cGood = gdk.color_parse("#000000") 914 self._cDiff = gdk.color_parse("#ff0000") 915 self._good = True 916 self._accept = accept or (isinstance(value, str) and str) or (lambda s: type(value)(eval(s))) 917 self.value = value
918 - def _add(self, text):
919 self._box.append_text(text)
920 - def _onPopup(self, *args):
921 self._prePopText = self._entry.get_text() 922 if self._box.get_property("popup-shown"): 923 self.repopulate()
924 - def repopulate(self):
925 self._box.handler_block(self._changed_handler) 926 for i in xrange(len(self._box.get_model())): 927 self._box.remove_text(0) 928 self.OnPopup(self, self._add) 929 self._box.handler_unblock(self._changed_handler)
930 - def combo_changed(self, combo):
931 active_i = combo.get_active() 932 if active_i >= 0: 933 text = combo.get_active_text() 934 combo.set_active(-1) 935 self._entry.set_text(self._prePopText) 936 if self.OnSuggest: 937 self.OnSuggest(self, text) 938 else: 939 self.value = self._accept(text) 940 self.OnChange(self, self.value)
941 - def setAccept(self, accept):
942 self._accept = accept 943 self.onParse()
944 accept = property(lambda s: s._accept, setAccept)
945 - def setValue(self, value, repr=None, good=True):
946 self._value = value 947 if repr is None: 948 self._str = self.format(value) 949 else: 950 self._str = repr 951 self.good = good 952 self._entry.set_text(self._str)
953 value = property(lambda s: s._value, setValue) 954 text = property(lambda s: s._str)
955 - def setGoodColor(self, c):
956 self._cGood = c 957 self._good = not self._good # force update 958 self.good = not self.good
959 cGood = property(lambda s: s._cGood, setGoodColor)
960 - def setDiffColor(self, c):
961 self._cDiff = c 962 self._good = not self._good # force update 963 self.good = not self.good
964 cDiff = property(lambda s: s._cDiff, setDiffColor)
965 - def setGood(self, good):
966 if self._good != good: 967 self._good = good 968 self._entry.modify_text(gtk.STATE_NORMAL, self._good and self._cGood or self._cDiff) 969 self._entry.modify_text(gtk.STATE_SELECTED, self._good and self._cGood or self._cDiff)
970 good = property(lambda s: s._good, setGood)
971 - def onKey(self, txt, event):
972 if event.keyval == keysyms.Escape: 973 txt.set_text(self._str) 974 txt.select_region(0, -1) 975 self.good = True 976 self.OnReset(self, self.value) 977 elif event.keyval == keysyms.Return: 978 if txt.get_text() == self._str: 979 self.OnChange(self, self.value) 980 self.good = True 981 else: 982 self.onParse() 983 elif (not (event.keyval in HARMLESS_KEYS)): 984 self.OnEdit(self, txt.get_text()) 985 self.good = False
986 - def onExit(self, txt, event):
987 self.onParse() 988 self.OnExit(self)
989 - def onParse(self):
990 s = self._entry.get_text() 991 if s == self._str: 992 self.good = True 993 self.OnReset(self, self.value) 994 else: 995 try: 996 self.setValue(self.accept(s), s) 997 self.OnChange(self, self.value) 998 self.good = True 999 self._entry.select_region(0, -1) 1000 except Exception, e: 1001 self.OnReject(self, s, e)
1002 # self._entry.set_text(self._str) # self.format(self.value)) 1003 1004
1005 -class SimpleList(gtk.TreeView):
1006 """ 1007 A gtk.TreeView specialized as a single-select single-column list. 1008 1009 @ivar popup: a gtk.Menu if they right-click, default None 1010 @ivar model: gtk.ListStore 1011 @ivar index: (unsorted) index of selected item 1012 @ivar active: text of the selected item, or None 1013 1014 @ivar OnSelect: L{WeakEvent}(SimpleList, ix) selected item graphically changed to ix'th 1015 @ivar OnDblClick: L{WeakEvent}(SimpleList, ix) ix'th item was double-clicked (or <enter> key) 1016 """ 1017 1018 __explore_featured = ['popup', 'model', 'index', 'active', 'OnSelect', 'OnDblClick', 1019 'get_selection', 'set_model', '_model', '_smodel', '_popup', 'indexOf', 1020 'clear', 'append', 'insert', 'edit', 'remove', 'select'] 1021
1022 - def __init__(self, header=None, sortable=True):
1023 """ 1024 @param header: caption for the top row 1025 @param sortable: default True: the user can click header to sort 1026 """ 1027 gtk.TreeView.__init__(self) 1028 self.set_headers_visible(bool(header)) 1029 self.get_selection().set_mode(gtk.SELECTION_SINGLE) 1030 self._index = -1 # pre-sorted 1031 self.OnSelect = WeakEvent() # SimpleList, ix # graphical select only 1032 self.OnDblClick = WeakEvent() # SimpleList, ix 1033 self.connect("cursor_changed", self._onCursorChanged) 1034 self.connect("row_activated", self._onRowActivated) 1035 self.connect('button_press_event', self._onButtonPress) 1036 self._popup = None 1037 self._model = gtk.ListStore(str) 1038 self._smodel = gtk.TreeModelSort(self._model) 1039 self.set_model(self._smodel) 1040 renderer = gtk.CellRendererText() 1041 column = gtk.TreeViewColumn(header or "", renderer, text=0) 1042 if sortable: 1043 column.set_sort_column_id(0) 1044 self._smodel.set_sort_column_id(0, gtk.SORT_ASCENDING) 1045 self.append_column(column)
1046 popup = property(lambda s: s._popup, lambda s,x: s._setPopup(x))
1047 - def _setPopup(self, x):
1048 self._popup = x
1049 model = property(lambda s: s._model) 1050 index = property(lambda s: s._index, lambda s,x: s._setIndex(x))
1051 - def _setIndex(self, ix):
1052 try: 1053 if ix >= 0: 1054 self.set_cursor(self._smodel.convert_child_path_to_path((ix,))) 1055 self._index = ix 1056 # self.get_selection().unselect_all() 1057 if ix >= 0: 1058 self.get_selection().select_path(self._smodel.convert_child_path_to_path((ix,))) 1059 else: 1060 self.get_selection().unselect_all() 1061 except: 1062 traceback.print_exc()
1063 active = property(lambda s: s.getActive(), lambda s,x: s.setActive(x))
1064 - def getActive(self):
1065 if self._index >= 0: 1066 return self._model.get(self._model.get_iter((self._index,)), 0)[0] 1067 else: 1068 return None
1069 - def setActive(self, x):
1070 self.index = self.indexOf(x)
1071 - def indexOf(self, x):
1072 """Returns the index of the first item with text x""" 1073 iter = self._model.get_iter_first() 1074 i = 0 1075 while iter: 1076 if self._model.get(iter, 0)[0] == x: 1077 return i 1078 iter = self._model.iter_next(iter) 1079 i += 1 1080 return -1
1081 - def clear(self):
1082 """Removes all items from the list.""" 1083 self._model.clear() 1084 self.index = -1 1085 self.OnSelect(self, self.index)
1086 - def append(self, x):
1087 """Adds x at the end of the list.""" 1088 self._model.append((x,))
1089 - def insert(self, i, x):
1090 self._model.insert(i, (x,))
1091 - def edit(self, index, newtext):
1092 """Replaces the text at index with newtext.""" 1093 self._model.set(self._model.get_iter((index,)), 0, newtext)
1094 - def remove(self, index):
1095 """Removes the item at index.""" 1096 self._model.remove(self._model.get_iter((index,))) 1097 if index == self.index: 1098 if index == len(self._model): 1099 index -= 1 1100 self.index = index 1101 self.OnSelect(self, self.index)
1102 - def select(self, i, s=True):
1103 """Selects the item at index i, or unselects if s==False.""" 1104 path = self._smodel.convert_child_path_to_path((i,)) 1105 if s: 1106 self.get_selection().select_path(path) 1107 else: 1108 self.get_selection().unselect_path(path)
1109 - def _onCursorChanged(self, tree):
1110 sel = self.get_selection() 1111 smodel, iter = sel.get_selected() 1112 if iter: 1113 ix = smodel.convert_path_to_child_path(smodel.get_path(iter))[0] 1114 if ix != self._index: 1115 self._index = ix 1116 self.OnSelect(self, self._index) 1117 else: 1118 if self._index >= 0: 1119 self._index = -1 1120 self.OnSelect(self, -1)
1121 - def _onRowActivated(self, tree, spath, col):
1122 path = self._smodel.convert_path_to_child_path(spath) 1123 if self._index != path[0]: 1124 self._index = path[0] 1125 self.OnSelect(self, self._index) 1126 self.OnDblClick(self, self._index)
1127 - def _onButtonPress(self, view, event):
1128 if self._popup and event.button == 3: 1129 # just in case the view doesn't alreay have focus 1130 view.grab_focus() 1131 selection = view.get_selection() 1132 tup = view.get_path_at_pos(int(event.x), int(event.y)) 1133 if not (tup is None): 1134 spath, col, cellx, celly = tup 1135 path = self._smodel.convert_path_to_child_path(spath) 1136 # if this row isn't already selected, then select it before popup 1137 if not selection.path_is_selected(spath): 1138 view.set_cursor( spath, col, 0) 1139 self._index = path[0] 1140 self._popup.popup(None, None, None, 0, event.time) 1141 return True
1142
1143 -class CheckList(gtk.TreeView):
1144 """ 1145 A gtk.TreeView specialized as a two-column display: checkbox and label 1146 1147 @ivar model: gtk.ListStore 1148 @ivar OnClick: L{WeakEvent}(index) 1149 @ivar OnToggle: L{WeakEvent}(index, active) 1150 1151 """ 1152 1153 __explore_featured = ['model', 'OnClick', 'OnToggle', '_index', 1154 'clear', 'append', 'insert', 'edit', 'remove', 'get_active', 'set_active'] 1155
1156 - def __init__(self, headers=[]):
1157 """ 1158 @param headers: list of captions for the top row 1159 """ 1160 gtk.TreeView.__init__(self) 1161 self.set_headers_visible(bool(headers)) 1162 self.get_selection().set_mode(gtk.SELECTION_SINGLE) 1163 self._index = -1 1164 self._model = gtk.ListStore(bool, str) 1165 self.set_model(self._model) 1166 self.connect("button_press_event", self.__onButtonPress) 1167 renderer = gtk.CellRendererToggle() 1168 renderer.set_property('activatable', True) 1169 renderer.connect( 'toggled', self.__toggled) 1170 column = gtk.TreeViewColumn(headers and headers[0] or "", renderer) 1171 column.add_attribute( renderer, "active", 0) 1172 self.append_column(column) 1173 self.colCheck = column 1174 renderer = gtk.CellRendererText() 1175 column = gtk.TreeViewColumn((len(headers)>1) and headers[1] or "", renderer, text=1) 1176 self.append_column(column) 1177 self.colNames = column 1178 self.OnToggle = WeakEvent() 1179 self.OnClick = WeakEvent()
1180 model = property(lambda s: s._model, lambda s, x: s._set_model(x))
1181 - def _set_model(self, x):
1182 self._model = x 1183 self.set_model(x)
1184 - def clear(self):
1185 """Removes all items from the list.""" 1186 self._model.clear()
1187 - def append(self, *item):
1188 """Adds x at the end of the list.""" 1189 self._model.append(item)
1190 - def insert(self, i, *item):
1191 self._model.insert(i, item)
1192 - def edit(self, index, newtext):
1193 """Replaces the text at index with newtext.""" 1194 self._model.set(self._model.get_iter((index,)), 1, newtext)
1195 - def remove(self, index):
1196 """Removes the item at index.""" 1197 self._model.remove(self._model.get_iter((index,)))
1198 - def get_active(self, index):
1199 return self._model[index][0]
1200 - def set_active(self, index, x):
1201 self._model[index][0] = x
1202 - def __toggled(self, cell, path):
1203 index = int(path) 1204 active = not self._model[path][0] 1205 self._model[path][0] = active 1206 self.OnToggle(index, active)
1207 - def __onButtonPress(self, view, event):
1208 try: 1209 path, col, cellx, celly = view.get_path_at_pos(int(event.x), int(event.y)) 1210 except: 1211 return False 1212 selection = view.get_selection() 1213 # if this row isn't already selected, then select it 1214 if col == self.colNames: 1215 if not selection.path_is_selected(path): 1216 view.set_cursor(path, col, False) 1217 self.OnClick(path[0]) 1218 return False
1219 1220
1221 -class OrderedCheckList(gtk.TreeView):
1222 """ 1223 A gtk.TreeView specialized as a two-column display: checkbox and label, plus columns with up and down arrows to modify order 1224 1225 @ivar model: gtk.ListStore 1226 @ivar OnClick: L{WeakEvent}(index) 1227 @ivar OnToggle: L{WeakEvent}(index, active) 1228 @ivar OnSwap: L{WeakEvent}(index0, index1) 1229 1230 """ 1231 1232 __explore_featured = ['model', 'OnClick', 'OnToggle', 'OnSwap', '_index', 1233 'clear', 'append', 'insert', 'edit', 'remove', 'get_active', 'set_active'] 1234
1235 - def __init__(self, headers=[]):
1236 """ 1237 @param headers: list of captions for the top row 1238 """ 1239 gtk.TreeView.__init__(self) 1240 self.set_headers_visible(bool(headers)) 1241 self.get_selection().set_mode(gtk.SELECTION_SINGLE) 1242 self._index = -1 1243 self._model = gtk.ListStore(bool, str) 1244 self.set_model(self._model) 1245 self.connect("button_press_event", self.__onButtonPress) 1246 renderer = gtk.CellRendererToggle() 1247 renderer.set_property('activatable', True) 1248 renderer.connect( 'toggled', self.__toggled) 1249 column = gtk.TreeViewColumn(headers and headers[0] or "", renderer) 1250 column.add_attribute( renderer, "active", 0) 1251 self.append_column(column) 1252 self.colCheck = column 1253 renderer = gtk.CellRendererText() 1254 column = gtk.TreeViewColumn((len(headers)>1) and headers[1] or "", renderer, text=1) 1255 column.set_expand(True) 1256 self.append_column(column) 1257 self.colNames = column 1258 renderer = gtk.CellRendererPixbuf() 1259 renderer.set_property('stock-id', gtk.STOCK_GO_UP) 1260 column = gtk.TreeViewColumn("", renderer) 1261 column.set_max_width(32) 1262 self.append_column(column) 1263 self.colUp = column 1264 renderer = gtk.CellRendererPixbuf() 1265 renderer.set_property('stock-id', gtk.STOCK_GO_DOWN) 1266 column = gtk.TreeViewColumn("", renderer) 1267 column.set_max_width(32) 1268 self.append_column(column) 1269 self.colDown = column 1270 self.append_column(column) 1271 self.OnToggle = WeakEvent() 1272 self.OnClick = WeakEvent() 1273 self.OnSwap = WeakEvent()
1274 model = property(lambda s: s._model, lambda s, x: s._set_model(x))
1275 - def _set_model(self, x):
1276 self._model = x 1277 self.set_model(x)
1278 - def clear(self):
1279 """Removes all items from the list.""" 1280 self._model.clear()
1281 - def append(self, *item):
1282 """Adds x at the end of the list.""" 1283 self._model.append(item)
1284 - def insert(self, i, *item):
1285 self._model.insert(i, item)
1286 - def edit(self, index, newtext):
1287 """Replaces the text at index with newtext.""" 1288 self._model.set(self._model.get_iter((index,)), 1, newtext)
1289 - def remove(self, index):
1290 """Removes the item at index.""" 1291 self._model.remove(self._model.get_iter((index,)))
1292 - def get_active(self, index):
1293 return self._model[index][0]
1294 - def set_active(self, index, x):
1295 self._model[index][0] = x
1296 - def __toggled(self, cell, path):
1297 index = int(path) 1298 active = not self._model[path][0] 1299 self._model[path][0] = active 1300 self.OnToggle(index, active)
1301 - def __onButtonPress(self, view, event):
1302 try: 1303 path, col, cellx, celly = view.get_path_at_pos(int(event.x), int(event.y)) 1304 except: 1305 return False 1306 selection = view.get_selection() 1307 # if this row isn't already selected, then select it 1308 if not selection.path_is_selected(path): 1309 view.set_cursor(path, col, False) 1310 self.OnClick(path[0]) 1311 1312 if col == self.colUp: 1313 if path[0]: 1314 row = list(self._model[path[0]]) 1315 self._model.remove(self._model.get_iter(path)) 1316 self._model.insert(path[0]-1, row) 1317 self.OnSwap(path[0]-1, path[0]) 1318 elif col == self.colDown: 1319 if path[0] < (len(self._model)-1): 1320 row = list(self._model[path[0]]) 1321 self._model.remove(self._model.get_iter(path)) 1322 self._model.insert(path[0]+1, row) 1323 self.OnSwap(path[0], path[0]+1) 1324 return False
1325 1326
1327 -class CountList(gtk.TreeView):
1328 """ 1329 A gtk.TreeView specialized as a two-column display: count and label 1330 1331 @ivar model: gtk.ListStore 1332 1333 """ 1334 1335 __explore_featured = ['model', 'colNames', 'OnToggle', 'OnClick', 1336 'clear', 'append', 'insert', 'edit', 'remove', 'get_count', 'set_count', 'get_entries'] 1337
1338 - def __init__(self, headers=[]):
1339 """ 1340 @param headers: list of captions for the top row 1341 """ 1342 gtk.TreeView.__init__(self) 1343 self.__ref = Reffer() 1344 self.set_headers_visible(bool(headers)) 1345 self.get_selection().set_mode(gtk.SELECTION_SINGLE) 1346 self._model = gtk.ListStore(int, str) 1347 self.set_model(self._model) 1348 renderer = gtk.CellRendererText() 1349 renderer.set_property('editable', True) 1350 renderer.connect('edited', self._onEdit) 1351 column = gtk.TreeViewColumn(headers and headers[0] or "", renderer, text=0) 1352 self.append_column(column) 1353 self.colCount = column 1354 renderer = gtk.CellRendererText() 1355 column = gtk.TreeViewColumn((len(headers)>1) and headers[1] or "", renderer, text=1) 1356 self.append_column(column) 1357 self.colNames = column 1358 self.OnToggle = WeakEvent() 1359 self.OnClick = WeakEvent()
1360 model = property(lambda s: s._model)
1361 - def clear(self):
1362 """Removes all items from the list.""" 1363 self._model.clear()
1364 - def append(self, count, lbl):
1365 """Adds x at the end of the list.""" 1366 self._model.append((count, lbl))
1367 - def insert(self, i, count, lbl):
1368 self._model.insert(i, (count, lbl))
1369 - def edit(self, index, newtext):
1370 """Replaces the text at index with newtext.""" 1371 self._model.set(self._model.get_iter((index,)), 1, newtext)
1372 - def remove(self, index):
1373 """Removes the item at index.""" 1374 self._model.remove(self._model.get_iter((index,)))
1375 - def get_count(self, index):
1376 return self._model[index][0]
1377 - def set_count(self, index, x):
1378 self._model[index][0] = x
1379 - def _onEdit(self, cell, path_string, new_text):
1380 i = int(path_string) 1381 try: 1382 self._model[i][0] = acceptIntGreaterThanOrEqualTo(0)(new_text) 1383 except: 1384 pass
1385 - def get_entries(self):
1386 """Returns list of (count, label).""" 1387 return [(self._model[i][0], self._model[i][1]) for i in xrange(len(self._model))]
1388
1389 -class MultiList(gtk.TreeView):
1390 """ 1391 A gtk.TreeView specialized as a multi-select, single-column list. 1392 1393 @ivar model: gtk.ListStore 1394 @ivar count: len(model) 1395 """ 1396 1397 __explore_featured = ['model', 'count', '_model', '_smodel', 'sortedToUn', 'unToSorted', 'indexOf', 1398 'clear', 'append', 'selected', 'select'] 1399
1400 - def __init__(self, header=None, sortable=True):
1401 """ 1402 @param header: caption for the top row 1403 @param sortable: default True: the user can click header to sort 1404 """ 1405 gtk.TreeView.__init__(self) 1406 self.set_rubber_banding(True) 1407 self.set_headers_visible(bool(header)) 1408 self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) 1409 self._count = 0 1410 self._model = gtk.ListStore(str) 1411 self._smodel = gtk.TreeModelSort(self._model) 1412 self.set_model(self._smodel) 1413 renderer = gtk.CellRendererText() 1414 column = gtk.TreeViewColumn(header or "", renderer, text=0) 1415 if sortable: 1416 column.set_sort_column_id(0) 1417 self._smodel.set_sort_column_id(0, gtk.SORT_ASCENDING) 1418 self.append_column(column)
1419 model = property(lambda s: s._model) 1420 count = property(lambda s: s._count)
1421 - def sortedToUn(self, ix):
1422 """Returns the unsorted index of the sorted ix'th item.""" 1423 return self._smodel.convert_path_to_child_path((ix,))[0]
1424 - def unToSorted(self, ix):
1425 """Returns the sorted index of the unsorted ix'th item.""" 1426 return self._smodel.convert_child_path_to_path((ix,))[0]
1427 - def _setIndex(self, ix):
1428 self._index = ix 1429 # self.get_selection().unselect_all() 1430 if ix >= 0: 1431 self.get_selection().select_path((self.unToSorted(ix),)) 1432 else: 1433 self.get_selection().unselect_all()
1434 - def indexOf(self, x):
1435 """Returns the unsorted index of the first item with text x.""" 1436 iter = self._model.get_iter_first() 1437 i = 0 1438 while iter: 1439 if self._model.get(iter, 0)[0] == x: 1440 return i 1441 iter = self._model.iter_next(iter) 1442 i += 1 1443 return -1
1444 - def clear(self):
1445 """Removes all items from the list.""" 1446 self._model.clear() 1447 self._count = 0
1448 - def append(self, x, s=True):
1449 """Adds item x to the end of the list; selects it if s == True.""" 1450 self._model.append((x,)) 1451 self._count += 1 1452 self.select(self._count-1, s)
1453 - def selected(self, i):
1454 """Returns True if unsorted item i if selected.""" 1455 return self.get_selection().path_is_selected((self.unToSorted(i),))
1456 - def select(self, i, s=True):
1457 """Selects unsorted item i; or unselects if s == False.""" 1458 if s: 1459 self.get_selection().select_path((self.unToSorted(i),)) 1460 else: 1461 self.get_selection().unselect_path((self.unToSorted(i),))
1462
1463 -def tree_model_sort_order(*cols):
1464 """Returns a gtk.TreeModel sort_func which sorts first by column i, then column j, ...""" 1465 def sort_cmp(model, a, b): 1466 for c in cols: 1467 ac, bc = [model.get(iter, c)[0] for iter in [a, b]] 1468 if ac < bc: 1469 return -1 1470 elif ac > bc: 1471 return 1 1472 return 0
1473 return sort_cmp 1474
1475 -class DictTable(gtk.TreeView):
1476 """ 1477 Specialization of a flat gtk.TreeView that can dynamically add columns for text or number. 1478 1479 @ivar popup: gtk.Menu for right-click, default None 1480 @ivar model: gtk.ListStore 1481 @ivar index: unsorted index of selected row 1482 @ivar count: len(model) 1483 1484 @ivar OnDblClick: L{WeakEvent}(DictTable, ix) 1485 """ 1486 1487 __explore_featured = ['popup', 'model', 'index', 'count', '_model', '_smodel', 'columns', 1488 '_col_of_name', '_name_of_col', '_type_of_col', '_used_col', '_spare_str', '_spare_float', 1489 'sortedToUn', 'unToSorted', 'clear', 'append', 'remove', 'selected', 'select', 1490 'select_all', 'unselect_all', 'copy'] 1491
1492 - def __init__(self, fixed, string_cap=10, float_cap=256):
1493 """ 1494 @param fixed: list of (name, type) of initially visible columns 1495 @param string_cap: max number of dynamically added string columns 1496 @param float_cap: max number of dynamically added number columns 1497 """ 1498 # one arg per fixed column: ('name', type) 1499 gtk.TreeView.__init__(self) 1500 self.set_rubber_banding(True) 1501 self.set_headers_visible(True) 1502 self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) 1503 self._count = 0 1504 self._index = -1 # pre-sorted 1505 self.OnDblClick = WeakEvent() # DictTable, ix 1506 self.connect("row_activated", self._onRowActivated) 1507 self.connect('button_press_event', self._onButtonPress) 1508 self.connect('key_press_event', self._onKeyPress) 1509 self._popup = None 1510 self._col_of_name = {} 1511 self._name_of_col = {} 1512 self._type_of_col = {} 1513 self._used_col = [] 1514 self._spare_str = [x for x in xrange(len(fixed), len(fixed)+string_cap)] 1515 self._spare_str.reverse() 1516 self._spare_float = [x for x in xrange(len(fixed)+string_cap, len(fixed)+string_cap+float_cap)] 1517 self._spare_float.reverse() 1518 types = [typ for nm, typ in fixed] + string_cap*[str] + float_cap*[float] 1519 self._model = gtk.ListStore(*types) 1520 self._smodel = gtk.TreeModelSort(self._model) 1521 self.set_model(self._smodel) 1522 self.columns = [] 1523 for i, fix in enumerate(fixed): 1524 nm, typ = fix 1525 renderer = gtk.CellRendererText() 1526 column = gtk.TreeViewColumn(nm, renderer, text=i) 1527 column.set_sort_column_id(i) 1528 self.append_column(column) 1529 self._col_of_name[nm] = i 1530 self._name_of_col[i] = nm 1531 self._type_of_col[i] = typ 1532 self._used_col.append(i) 1533 self.columns.append(column) 1534 self._smodel.set_sort_column_id(0, gtk.SORT_ASCENDING)
1535 popup = property(lambda s: s._popup, lambda s,x: s._setPopup(x)) 1536 model = property(lambda s: s._model) 1537 index = property(lambda s: s._getIndex(), lambda s,x: s._setIndex(x)) 1538 count = property(lambda s: s._count)
1539 - def sortedToUn(self, ix):
1540 """Returns the unsorted index of the ix'th sorted row.""" 1541 return self._smodel.convert_path_to_child_path((ix,))[0]
1542 - def unToSorted(self, ix):
1543 """Returns the sorted index of the ix'th unsorted row.""" 1544 return self._smodel.convert_child_path_to_path((ix,))[0]
1545 - def _setPopup(self, x):
1546 self._popup = x
1547 - def _getIndex(self):
1548 if self._index >= 0 and self.selected(self._index): 1549 return self._index 1550 for i in xrange(self._count): 1551 if self.selected(i): 1552 self._index = i 1553 return i 1554 self._index = -1 1555 return -1
1556 - def _setIndex(self, ix):
1557 self._index = ix 1558 # self.get_selection().unselect_all() 1559 if ix >= 0: 1560 self.get_selection().select_path(self._smodel.convert_child_path_to_path((ix,))) 1561 else: 1562 self.get_selection().unselect_all()
1563 - def clear(self):
1564 """Removes all rows.""" 1565 self._model.clear() 1566 self._count = 0
1567 - def append(self, d):
1568 """Adds a row described by dictionary d, where 1569 d[column_name] = column_value.""" 1570 entry = [None] * (len(self._used_col) + len(self._spare_str) + len(self._spare_float)) 1571 for i in self._used_col: 1572 entry[i] = self._type_of_col[i]() 1573 for i in self._spare_str: 1574 entry[i] = "" 1575 for i in self._spare_float: 1576 entry[i] = 0.0 1577 for nm in sorted(d.iterkeys()): 1578 val = d[nm] 1579 try: 1580 entry[self._col_of_name[nm]] = val 1581 except: 1582 if isinstance(val, str): 1583 typ = str 1584 ix = self._spare_str.pop() 1585 else: 1586 typ = float 1587 ix = self._spare_float.pop() 1588 renderer = gtk.CellRendererText() 1589 column = gtk.TreeViewColumn(nm, renderer, text=ix) 1590 column.set_sort_column_id(ix) 1591 self.append_column(column) 1592 self._col_of_name[nm] = ix 1593 self._name_of_col[ix] = nm 1594 self._type_of_col[ix] = typ 1595 self._used_col.append(ix) 1596 self.columns.append(column) 1597 entry[ix] = val 1598 self._model.append(entry) 1599 self._count += 1
1600 - def remove(self, i):
1601 """Removes the item with unsorted index i.""" 1602 self._model.remove(self._model.get_iter((i,))) 1603 self._count -= 1
1604 - def selected(self, i):
1605 """Returns True if the i'th unsorted row is selected.""" 1606 return self.get_selection().path_is_selected(self._smodel.convert_child_path_to_path((i,)))
1607 - def select(self, i, s=True):
1608 """Selects the i'th unsorted row, or unselect if s == False.""" 1609 path = self._smodel.convert_child_path_to_path((i,)) 1610 if s: 1611 self.get_selection().select_path(path) 1612 else: 1613 self.get_selection().unselect_path(path)
1614 - def select_all(self):
1615 """Selects all rows.""" 1616 self.get_selection().select_all()
1617 - def unselect_all(self):
1618 """Unselects all rows.""" 1619 self.get_selection().unselect_all()
1620 - def copy(self):
1621 """Copies all non-empty columns of all selected rows as tab-selected text, to clipboard.""" 1622 sel = [i for i in xrange(self._count) if self.selected(i)] 1623 #def has_nonzero(c): 1624 # return True in [bool(self._model.get(self._model.get_iter((i)), c)[0]) for i in sel] 1625 col = [i for i in self._used_col] # if ((self._type_of_col[i] == str) or has_nonzero(i))] 1626 # copy selected rows, str #and zero# and nonzero columns, to clipboard 1627 lines = [] 1628 toks = [] 1629 for c in col: 1630 toks.append(self._name_of_col[c]) 1631 lines.append(toks) 1632 for i in xrange(self._count): 1633 r = self.sortedToUn(i) 1634 if r in sel: 1635 toks = [] 1636 for c in col: 1637 toks.append(str(self._model.get(self._model.get_iter((r)), c)[0])) 1638 lines.append(toks) 1639 lines.append([]) 1640 text = '\n'.join(['\t'.join(toks) for toks in lines]) 1641 clipboard = gtk.clipboard_get(gdk.SELECTION_CLIPBOARD) 1642 clipboard.set_text(text)
1643 - def _onRowActivated(self, tree, spath, col):
1644 path = self._smodel.convert_path_to_child_path(spath) 1645 if self._index != path[0]: 1646 self._index = path[0] 1647 self.OnDblClick(self, self._index)
1648 - def _onButtonPress(self, view, event):
1649 if self._popup and event.button == 3: 1650 # just in case the view doesn't alreay have focus 1651 view.grab_focus() 1652 selection = view.get_selection() 1653 tup = view.get_path_at_pos(int(event.x), int(event.y)) 1654 if not (tup is None): 1655 spath, col, cellx, celly = tup 1656 path = self._smodel.convert_path_to_child_path(spath) 1657 # if this row isn't already selected, then select it before popup 1658 if not selection.path_is_selected(spath): 1659 selection.select_path(spath) 1660 self._index = path[0] 1661 self._popup.popup(None, None, None, 0, event.time) 1662 return True
1663 - def _onKeyPress(self, view, event):
1664 # apparently ctrl-a is already select-all 1665 if event.state & gdk.CONTROL_MASK: 1666 if event.keyval == ord('c') or event.keyval == ord('C'): 1667 self.copy()
1668 1669
1670 -def aspect_dimensions(w, h, n, aspect=1.618):
1671 r = c = 1 1672 w_one, h_one = w, h 1673 while (r*c) < n: 1674 if aspect < (w_one*1.0/h_one): 1675 c += 1 1676 w_one = max(1, w/c) 1677 else: 1678 r += 1 1679 h_one = max(1, h/r) 1680 while (r > 1) and ((r-1)*c >= n): 1681 r -= 1 1682 return r, c
1683
1684 -class AspectGrid(gtk.Table):
1685 __explore_featured = ['aspect', 'resize_aspect', 'pack_aspect', 'remove']
1686 - def __init__(self, aspect=1.618):
1687 gtk.Table.__init__(self, 1, 1, homogeneous=True) 1688 self.connect('size_allocate', self.__on_size_allocate) 1689 self.aspect = aspect 1690 self.__children = [] 1691 self.__size = (1, 1) # pixels 1692 self.__dim = (1, 1) # rows, cols
1693 - def __on_size_allocate(self, *args):
1694 self.resize_aspect()
1695 - def resize_aspect(self, width=None, height=None, nchild=None):
1696 w = width or self.allocation.width or 1 1697 h = height or self.allocation.height or 1 1698 n = nchild or len(self.__children) 1699 r, c = aspect_dimensions(w, h, n, self.aspect) 1700 if (r, c) != self.__dim: 1701 self.__dim = (r, c) 1702 for child in self.__children: 1703 gtk.Table.remove(self, child) 1704 self.resize(r, c) 1705 i = j = 0 1706 for child in self.__children: 1707 self.attach(child, j, j+1, i, i+1) 1708 j += 1 1709 if j == c: 1710 j = 0 1711 i += 1
1712 - def pack_aspect(self, child, index=None): # index only works if it was the last index removed, with temporary=True
1713 self.resize_aspect(nchild=len(self.__children)+1) 1714 ix = len(self.__children) 1715 if not (index is None): 1716 ix = index 1717 i = ix / self.__dim[1] 1718 j = ix % self.__dim[1] 1719 self.__children.insert(ix, child) 1720 self.attach(child, j, j+1, i, i+1)
1721 - def remove(self, child, temporary=False):
1722 index = self.__children.index(child) 1723 gtk.Table.remove(self, child) 1724 self.__children.remove(child) 1725 if not temporary: 1726 self.resize_aspect() 1727 return index
1728 1729
1730 -class ZoomBar(gtk.HBox):
1731 """ 1732 Widget combining H scrollbar with zoom in/out buttons. 1733 1734 @ivar range: tuple (lo, hi) the portion of (0, 100) that should be shown 1735 @ivar zoom: percent magnification, where 100% means all-on-screen 1736 @ivar adjustment: lower = range[0], upper = range[1]; connect to 'changed' and 'value_changed' 1737 """ 1738 1739 __explore_featured = ['range', 'zoom', 'adjustment', 'txtPct', 'btnMinus', 'btnPlus', 'scroll'] 1740
1741 - def __init__(self, zoom_min=100.0):
1742 """ 1743 @param zoom_min: smallest allowed zoom value 1744 """ 1745 gtk.HBox.__init__(self) 1746 self.zoom_min = zoom_min 1747 self._range = (0.0, 100.0) 1748 self._zoom = 100.0 1749 self.r = Reffer() 1750 dist = self._range[1] - self._range[0] 1751 self.adjustment = gtk.Adjustment( value=0, lower=self._range[0], upper=self._range[1], 1752 step_incr=dist/1000, page_incr=dist, page_size=dist ) 1753 self.txtPct = pack_item(NumEntry(100.0, self._acceptPct, "%.0f", width_chars=4), self) 1754 self.txtPct.OnChange += self.r(self._onChangePct) 1755 self.btnMinus = pack_button('-', self, self._onPressMinus) 1756 self.btnPlus = pack_button('+', self, self._onPressPlus) 1757 self.scroll = pack_item(gtk.HScrollbar(self.adjustment), self, expand=True) 1758 self._setting = False
1759 - def set_range(self, r):
1760 self._range = r 1761 self._recalc()
1762 - def set_zoom(self, z):
1763 self._zoom = self._acceptPct(z) 1764 self._setting = True 1765 self.txtPct.setValue(self._zoom) 1766 self._setting = False 1767 self._recalc()
1768 range = property(lambda s: s._range, set_range) 1769 zoom = property(lambda s: s._zoom, set_zoom)
1770 - def _recalc(self):
1771 lo, hi = self._range 1772 d = hi - lo 1773 z = self._zoom 1774 a = self.adjustment 1775 a.lower, a.upper = lo, hi 1776 a.step_increment = d/(10*z) 1777 a.page_increment = d*100/z 1778 a.page_size = d*100/z 1779 a.emit('changed') 1780 a.set_value(min(a.value, a.upper-a.page_size))
1781 - def _acceptPct(self, pctStr):
1782 pct = float(pctStr) 1783 if pct < self.zoom_min: 1784 raise ValueError('zoom percent must be >= %f' % self._zoom_min) 1785 return pct
1786 - def _onChangePct(self, txt, pct):
1787 if self._setting: return 1788 self.zoom = pct
1789 - def _onPressMinus(self, widget):
1790 z = self.zoom / 2 1791 if z >= self.zoom_min: 1792 self.zoom = z
1793 - def _onPressPlus(self, widget):
1794 self.zoom *= 2
1795 1796
1797 -def draw_inscribed_triangle(cr, w, h, color, angle, goal=None):
1798 cr.save() 1799 rad = max(1e-1, min(w,h)/2.0) 1800 # law of sines: side/sin(opposite) = circumscribing diameter 1801 side = 2*rad * sin(pi/3) 1802 corner_y = side/2 1803 # for what angle is normalized half a side the x coord (cos)? 1804 corner_theta = pi - asin(corner_y/rad) 1805 corner_x = cos(corner_theta) * rad 1806 cr.translate(w/2.0, h/2.0) 1807 cr.rotate(- angle) 1808 if (not (goal is None)) and (goal == angle): 1809 cr.set_source_rgb(*color) 1810 else: 1811 cr.set_source_rgba(*(list(color)+[.7])) 1812 cr.move_to(rad, 0) 1813 cr.line_to(corner_x, corner_y) 1814 cr.line_to(corner_x, -corner_y) 1815 cr.fill_preserve() 1816 cr.set_source_rgb(0,0,0) 1817 cr.set_line_width(0.5) 1818 cr.stroke() 1819 cr.restore()
1820
1821 -def draw_inscribed_arrow(cr, w, h, draw_circle=True, point_theta=0.0, back_dtheta=9*pi/20, shaft_w_rads=.4):
1822 rad = min(w, h)/2.0 1823 cr.save() 1824 cr.translate(w/2.0, h/2.0) 1825 cr.rotate(- point_theta) 1826 if draw_circle: 1827 cr.arc(0, 0, rad, 0, 2*pi) 1828 cr.stroke() 1829 ys = shaft_w_rads * rad / 2 1830 rad -= ys 1831 cr.move_to(rad, 0) 1832 xb = rad*cos(back_dtheta) 1833 yb = rad*sin(back_dtheta) 1834 cr.line_to(xb, yb) 1835 cr.line_to(xb, -yb) 1836 cr.line_to(rad, 0) 1837 cr.fill() 1838 cr.rectangle(-rad, -ys, rad+xb, 2*ys) 1839 cr.fill() 1840 cr.restore()
1841 1842
1843 -class Triangle(gtk.DrawingArea):
1844 """ 1845 Shows a triangle; animates when you change angle. 1846 1847 @ivar angle: in radians 1848 @ivar color: (r, g, b) in [0..1] 1849 """ 1850 1851 __explore_featured = ['angle', 'color', 'invalidate', 'animate'] 1852
1853 - def __init__(self, angle=-pi/2, color=(0.2, 1.0, 0.2)):
1854 gtk.DrawingArea.__init__(self) 1855 self.set_size_request(12, 12) 1856 self.connect('expose_event', self._expose) 1857 self._angle = angle 1858 self._color = color 1859 self._goal = angle
1860 - def set_angle(self, angle):
1861 self._goal = angle 1862 if self.window: 1863 self.animate() 1864 else: 1865 self._angle = self._goal
1866 angle = property(lambda s: s._angle, set_angle)
1867 - def set_color(self, color):
1868 self._color = color 1869 self.invalidate()
1870 color = property(lambda s: s._color, set_color)
1871 - def invalidate(self):
1872 """Forces redraw.""" 1873 if not self.window: return 1874 alloc = self.get_allocation() 1875 rect = gdk.Rectangle(0, 0, alloc.width, alloc.height) 1876 self.window.invalidate_rect(rect, True)
1877 - def animate(self):
1878 if self._angle != self._goal: 1879 sign = (self._goal > self._angle) and 1.0 or -1.0 1880 # self._angle += sign * min(pi/20, abs(self._goal - self._angle)) 1881 self._angle += 0.2 * (self._goal - self._angle) 1882 self.invalidate() 1883 #if self._angle != self._goal: 1884 if abs(self._angle - self._goal) < 1e-1: 1885 self._angle = self._goal 1886 else: 1887 gobject.timeout_add(20, self.animate)
1888 - def _expose(self, widget, event):
1889 w,h = self.window.get_size() 1890 cr = self.window.cairo_create() 1891 draw_inscribed_triangle(cr, w, h, self._color, self._angle, self._goal)
1892
1893 -class TriangleButton(gtk.Button):
1894 """gtk.Button decorated with a Triangle.""" 1895 1896 __explore_featured = ['triangle', 'label'] 1897
1898 - def __init__(self, angle=-pi/2, color=(0.2, 1.0, 0.2), label=''):
1899 gtk.Button.__init__(self) 1900 h = gtk.HBox() 1901 h.show() 1902 self.add(h) 1903 self.triangle = pack_item(Triangle(angle, color), h, expand=True) 1904 pack_space(h, x=5, expand=False) 1905 self.label = pack_label(label, h)
1906
1907 -class TriangleExpander(TriangleButton):
1908 __explore_featured = ['expanded']
1909 - def __init__(self, *args, **kw):
1910 TriangleButton.__init__(self, *args, **kw) 1911 self.connect('clicked', self._onClick) 1912 self._expanded = False 1913 self.OnExpanded = WeakEvent() # (btn, expanded) 1914 self.triangle.angle = 0
1915 expanded = property(lambda self: self._expanded, lambda self, x: self.set_expanded(x))
1916 - def set_expanded(self, x):
1917 if self._expanded == x: return 1918 self._expanded = x 1919 self.triangle.angle = x and (-pi/2) or 0 1920 self.OnExpanded(self, x)
1921 - def _onClick(self, btn):
1922 self.expanded = not self.expanded
1923 1924 1925
1926 -class TextViewAppender(object):
1927 """File-like object which appends to a gtk.TextView, optionally formatted, and scrolls to the end.""" 1928 __explore_featured = ['tv', 'OnWrite', 'write', 'write_bold']
1929 - def __init__(self, tv):
1930 self.tv = tv 1931 tv.get_buffer().create_tag("bold", weight=pango.WEIGHT_BOLD) 1932 self.OnWrite = WeakEvent() # (text)
1933 - def write(self, msg, scroll=True):
1934 buf = self.tv.get_buffer() 1935 self.OnWrite(msg) 1936 buf.insert(buf.get_end_iter(), msg)# [:end]+'\n') 1937 if scroll: 1938 self.tv.scroll_to_mark(buf.get_insert(), 0, True)
1939 - def write_bold(self, msg, scroll=True):
1940 buf = self.tv.get_buffer() 1941 self.OnWrite(msg) 1942 buf.insert_with_tags_by_name(buf.get_end_iter(), msg, "bold") 1943 if scroll: 1944 self.tv.scroll_to_mark(buf.get_insert(), 0)
1945 - def __call__(self, msg):
1946 self.write(msg)
1947
1948 -def SetFixedWidth(textView, points=10):
1949 """Changes the font of a gtk.TextView to Monospace 10.""" 1950 pangoFont = pango.FontDescription('monospace %i' % points) 1951 textView.modify_font(pangoFont)
1952
1953 -def SetClipboardCustom(clipboard, target, bytes):
1954 """Puts custom data on the specified clipboard, identified by target string.""" 1955 targets = [ (target, 0, 0) ] 1956 def get_func(clipboard, selectiondata, info, data): 1957 selectiondata.set_text(data) 1958 return
1959 def clear_func(clipboard, data): 1960 del data 1961 return 1962 clipboard.set_with_data(targets, get_func, clear_func, bytes) 1963 1964
1965 -class CopyDialog(gtk.Dialog):
1966 """A customizable copy image dialog"""
1967 - def __init__(self, title='Copy', parent=None):
1968 gtk.Dialog.__init__(self, title, parent or get_active_window(), gtk.DIALOG_MODAL, 1969 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 1970 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) 1971 self.set_default_response(gtk.RESPONSE_ACCEPT) 1972 self.__ref = Reffer() 1973 self.basic_controls = pack_item(gtk.VBox(), self.vbox) 1974 frame = pack_item(gtk.Frame('Size'), self.basic_controls) 1975 fbox = gtk.VBox() 1976 fbox.show() 1977 frame.add(fbox) 1978 self.chkAuto = pack_check('auto', fbox, True, self.__onToggleAuto) 1979 line = pack_item(gtk.HBox(True), fbox) 1980 pack_label('Width:', line, expand=True) 1981 self.txtWidth = pack_item(NumEntry(1, acceptIntGreaterThan(0), width_chars=11), line, expand=True) 1982 self.txtWidth.OnChange += self.__ref(self.__onChangeSize) 1983 line = pack_item(gtk.HBox(True), fbox) 1984 pack_label('Height:', line, expand=True) 1985 self.txtHeight = pack_item(NumEntry(1, acceptIntGreaterThan(0), width_chars=11), line, expand=True) 1986 self.txtHeight.OnChange += self.__ref(self.__onChangeSize)
1987 - def __onToggleAuto(self, chk):
1988 if chk.get_active(): 1989 w, h = self.view.window.get_size() 1990 self.txtWidth.setValue(w) 1991 self.txtHeight.setValue(h)
1992 - def __onChangeSize(self, txt, val):
1993 self.chkAuto.set_active(False)
1994 - def run(self, view):
1995 self.view = view 1996 if self.chkAuto.get_active(): 1997 w, h = view.window.get_size() 1998 self.txtWidth.setValue(w) 1999 self.txtHeight.setValue(h) 2000 response = gtk.Dialog.run(self) 2001 self.hide() 2002 self.view = None 2003 self.copy_width = self.txtWidth.value 2004 self.copy_height = self.txtHeight.value 2005 return response
2006 2007 2008 2009
2010 -def RGBAtoGDK(tup):
2011 return gdk.Color(*[int(round(x*65535.0)) for x in tup[:3]])
2012 -def GDKtoRGBA(color):
2013 return tuple([x/65535.0 for x in [color.red, color.green, color.blue]] + [1])
2014 2015
2016 -def ShowMessage(message, buttons=gtk.BUTTONS_OK, title="", parent=None):
2017 mdlg = gtk.MessageDialog(parent or get_active_window(), buttons=buttons, flags=gtk.DIALOG_MODAL, message_format=message) 2018 response = mdlg.run() 2019 mdlg.destroy() 2020 return response
2021
2022 -def PromptChoices(message, choices=['Cancel', 'OK'], title="", parent=None):
2023 """Returns index of choice; 0 is also window-close.""" 2024 dlg = gtk.Dialog(title, parent or get_active_window(), gtk.DIALOG_MODAL) 2025 pack_label(message, dlg.vbox, fill=False) 2026 for i, choice in enumerate(choices): 2027 dlg.add_button(choice, i) 2028 response = dlg.run() 2029 if response == gtk.RESPONSE_REJECT: 2030 response = 0 2031 dlg.destroy() 2032 return response
2033
2034 -def PromptEntry(message, value, accept=None, format='%s', title="", parent=None):
2035 """Returns modified value, or None if canceled.""" 2036 dlg = NumEntryDialog(title=title, parent=parent or get_active_window(), message=message, value=value, accept=accept, format=format) 2037 response = dlg.run() 2038 dlg.destroy() 2039 if response == gtk.RESPONSE_ACCEPT: 2040 return dlg.value 2041 else: 2042 return None
2043
2044 -def PromptEntries(items, title="", parent=None):
2045 """Returns modified value list, or None if canceled. 2046 2047 @param items: list of tuple (caption, value, accept, format) 2048 """ 2049 dlg = NumEntriesDialog(title=title, parent=parent or get_active_window(), items=[ [i]+list(it) for i,it in enumerate(items) ]) 2050 response = dlg.run() 2051 result = [dlg.values[i] for i in xrange(len(items))] 2052 dlg.destroy() 2053 if response == gtk.RESPONSE_ACCEPT: 2054 return result 2055 else: 2056 return None
2057 2058
2059 -class Requestable(object):
2060 """Defines a resource which is calculated when needed and cached until invalidated; 2061 it may take a long time to calculate, so request is asynchronous -- 2062 you provide a receiver callback, which is called by gobject some time later. 2063 """
2064 - def __init__(self, get=None):
2065 """ 2066 2067 @param get: lambda receiver: gobject.idle_add(receiver, answer_part_1, answer_part_2, ...) -- computation function to replace self.get (or you can inherit this class and override self.get) 2068 """ 2069 self.serial = 0 2070 self.__get = get or self.get 2071 self.__last_serial = -1 2072 self.__last_val = None 2073 self.OnInvalidate = WeakEvent() # (Product)
2074 - def invalidate(self):
2075 """Dumps any cached value; next request will recompute.""" 2076 self.serial += 1 2077 self.__last_val = None 2078 self.OnInvalidate(self)
2079 - def request(self, receiver):
2080 """Requests that the value be calculated if needed, then receiver(answer_part_1, answer_part_2, ...) 2081 is called by gobject.""" 2082 if self.serial != self.__last_serial: 2083 self.__receiver = receiver 2084 self.__recv_serial = self.serial 2085 def receive(*args): 2086 self.__last_serial = self.__recv_serial 2087 self.__last_val = args 2088 receiver(*args)
2089 gobject.idle_add(self.__get, receive) 2090 else: 2091 gobject.idle_add(receiver, *self.__last_val)
2092 - def get(self, receiver):
2093 """Override this method to compute the answer(s) and pass them via gobject; e.g.: 2094 2095 >>> val = 2+2 2096 >>> val2 = 2*2 2097 >>> gobject.idle_add(receiver, val, val2) # it's up to you how many args are passed 2098 2099 Alternatively, pass get=lambda receiver: ... to the constructor. 2100 """ 2101 raise Exception('abstract')
2102 2103
2104 -def ChooseFolder(caption, path, parent=None):
2105 dlg = gtk.FileChooserDialog(caption, parent or get_active_window(), gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, 2106 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) 2107 dlg.set_default_response(gtk.RESPONSE_OK) 2108 dlg.set_current_folder(path) 2109 response = dlg.run() 2110 path = dlg.get_filename() 2111 dlg.destroy() 2112 if response == gtk.RESPONSE_OK: 2113 return path 2114 else: 2115 return None
2116 2117
2118 -def Open(caption, path, do_open, filters=[('Text files', '.txt')], parent=None, allow_all_files=True, do_on_cancel=lambda: None):
2119 dlg = gtk.FileChooserDialog(caption, parent or get_active_window(), gtk.FILE_CHOOSER_ACTION_OPEN, 2120 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) 2121 dlg.set_default_response(gtk.RESPONSE_OK) 2122 for lbl, ext in filters: 2123 filter = gtk.FileFilter() 2124 filter.set_name(lbl) 2125 filter.add_pattern('*'+ext) 2126 dlg.add_filter(filter) 2127 if allow_all_files: 2128 filter = gtk.FileFilter() 2129 filter.set_name('All files') 2130 filter.add_pattern('*.*') 2131 dlg.add_filter(filter) 2132 dlg.set_filter(filter) 2133 dlg.set_current_folder(path) 2134 response = dlg.run() 2135 newpath = dlg.get_current_folder() 2136 newname = dlg.get_filename() 2137 dlg.destroy() 2138 if response != gtk.RESPONSE_OK: 2139 do_on_cancel() 2140 return "" 2141 fname = newname 2142 path = newpath 2143 if fname: 2144 try: 2145 do_open(fname) 2146 return fname 2147 except: 2148 mdlg = gtk.MessageDialog(None, buttons=gtk.BUTTONS_OK, flags=gtk.DIALOG_MODAL, 2149 message_format= "Error opening %s:\n%s" % (fname, traceback.format_exc())) 2150 mdlg.run() 2151 mdlg.destroy() 2152 fname = "" 2153 do_on_cancel() 2154 return fname
2155
2156 -def OpenMulti(caption, path, do_open, filters=[('Text files', '.txt')], parent=None, allow_all_files=True):
2157 dlg = gtk.FileChooserDialog(caption, parent or get_active_window(), gtk.FILE_CHOOSER_ACTION_OPEN, 2158 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) 2159 dlg.set_select_multiple(True) 2160 dlg.set_default_response(gtk.RESPONSE_OK) 2161 for lbl, ext in filters: 2162 filter = gtk.FileFilter() 2163 filter.set_name(lbl) 2164 filter.add_pattern('*'+ext) 2165 dlg.add_filter(filter) 2166 if allow_all_files: 2167 filter = gtk.FileFilter() 2168 filter.set_name('All files') 2169 filter.add_pattern('*.*') 2170 dlg.add_filter(filter) 2171 dlg.set_current_folder(path) 2172 response = dlg.run() 2173 newpath = dlg.get_current_folder() 2174 names = dlg.get_filenames() 2175 dlg.destroy() 2176 if response != gtk.RESPONSE_OK: 2177 return [] 2178 path = newpath 2179 if names: 2180 try: 2181 do_open(names) 2182 return names 2183 except: 2184 mdlg = gtk.MessageDialog(None, buttons=gtk.BUTTONS_OK, flags=gtk.DIALOG_MODAL, 2185 message_format= "Error opening list:\n%s" % traceback.format_exc()) 2186 mdlg.run() 2187 mdlg.destroy() 2188 names = [] 2189 return names
2190 2191
2192 -def SaveAs(caption, path, name, do_save, filters=[('Text files', '.txt')], caption_if_exists='Overwrite', parent=None, 2193 allow_all_files=False):
2194 fname = name 2195 while True: 2196 dlg = gtk.FileChooserDialog(caption, parent or get_active_window(), gtk.FILE_CHOOSER_ACTION_SAVE, 2197 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) 2198 dlg.set_default_response(gtk.RESPONSE_OK) 2199 filter_objs = [] 2200 for lbl, ext in filters: 2201 filter = gtk.FileFilter() 2202 filter.set_name(lbl) 2203 filter.add_pattern('*'+ext) 2204 dlg.add_filter(filter) 2205 filter_objs.append(filter) 2206 if allow_all_files: 2207 filter = gtk.FileFilter() 2208 filter.set_name('All files') 2209 filter.add_pattern('*.*') 2210 dlg.add_filter(filter) 2211 if filter_objs: 2212 dlg.set_filter(filter_objs[0]) 2213 dlg.set_current_folder(path) 2214 dlg.set_current_name(fname) 2215 response = dlg.run() 2216 if response == gtk.RESPONSE_OK: 2217 newpath = dlg.get_current_folder() 2218 newname = dlg.get_filename() 2219 fname = newname 2220 path = newpath 2221 if True: # not os.path.splitext(fname)[1]: 2222 chosen_filter = dlg.get_filter() 2223 for i, fi in enumerate(filter_objs): 2224 if fi == chosen_filter: 2225 fname = os.path.splitext(fname)[0] + filters[i][1] 2226 break 2227 dlg.destroy() 2228 if response != gtk.RESPONSE_OK: 2229 return "" 2230 if os.path.exists(fname): 2231 mdlg = gtk.MessageDialog(parent or get_active_window(), buttons=gtk.BUTTONS_YES_NO, 2232 flags=gtk.DIALOG_MODAL, 2233 message_format= "The file exists. %s it?" % caption_if_exists) 2234 response = mdlg.run() 2235 mdlg.destroy() 2236 if response == gtk.RESPONSE_NO: 2237 fname = "" 2238 if fname: 2239 try: 2240 do_save(fname) 2241 return fname 2242 except: 2243 mdlg = gtk.MessageDialog(None, buttons=gtk.BUTTONS_OK, flags=gtk.DIALOG_MODAL, 2244 message_format= "Error saving %s:\n%s" % (fname, traceback.format_exc())) 2245 mdlg.run() 2246 mdlg.destroy() 2247 fname = ""
2248 2249
2250 -def pack_item(item, container, expand=False, fill=True, show=True, at_end=False):
2251 if show: 2252 item.show() 2253 if not (container is None): 2254 (at_end and container.pack_end or container.pack_start)(item, expand, fill) 2255 return item
2256
2257 -def pack_space(container, x=-1, y=-1, expand=False, fill=True, show=True, at_end=False):
2258 sp = gtk.EventBox() 2259 sp.set_size_request(x, y) 2260 sp.show() 2261 return pack_item(sp, container, expand, fill, show, at_end)
2262
2263 -def pack_hsep(pix, container, x=-1, expand=False, fill=True, show=True, at_end=False):
2264 sep = gtk.HSeparator() 2265 sep.set_size_request(x, pix) 2266 return pack_item(sep, container, expand, fill, show, at_end)
2267
2268 -def pack_vsep(pix, container, y=-1, expand=False, fill=True, show=True, at_end=False):
2269 sep = gtk.VSeparator() 2270 sep.set_size_request(pix, y) 2271 return pack_item(sep, container, expand, fill, show, at_end)
2272
2273 -def pack_label(text, container, expand=False, fill=True, show=True, at_end=False):
2274 return pack_item(gtk.Label(text), container, expand, fill, show, at_end)
2275
2276 -def pack_button(text, container, on_click=lambda btn: None, sensitive=True, expand=False, fill=True, show=True, at_end=False):
2277 btn = gtk.Button(text) 2278 btn.set_sensitive(sensitive) 2279 btn.evt_clicked = btn.connect('clicked', on_click) 2280 return pack_item(btn, container, expand, fill, show, at_end)
2281
2282 -def pack_check(text, container, active=False, on_toggle=None, expand=False, fill=True, show=True, at_end=False):
2283 chk = gtk.CheckButton(text) 2284 if active: 2285 chk.set_active(True) 2286 if on_toggle: 2287 chk.evt_toggled = chk.connect('toggled', on_toggle) 2288 return pack_item(chk, container, expand, fill, show, at_end)
2289
2290 -def pack_radio(text, container, group=None, active=False, on_toggle=None, expand=False, fill=True, show=True, at_end=False):
2291 rad = gtk.RadioButton(group, text) 2292 if active: 2293 rad.set_active(True) 2294 if on_toggle: 2295 rad.evt_toggled = rad.connect('toggled', on_toggle) 2296 return pack_item(rad, container, expand, fill, show, at_end)
2297
2298 -def pack_scrolled(item, container, with_vp=False, x_policy=gtk.POLICY_AUTOMATIC, y_policy=gtk.POLICY_AUTOMATIC, size_request=(-1, -1), expand=False, fill=True, show=True, at_end=False):
2299 scrolled = gtk.ScrolledWindow() 2300 scrolled.set_policy(x_policy, y_policy) 2301 scrolled.set_size_request(*size_request) 2302 if with_vp: 2303 scrolled.add_with_viewport(item) 2304 else: 2305 scrolled.add(item) 2306 item.show() 2307 return pack_item(scrolled, container, expand, fill, show, at_end)
2308
2309 -def build_menuitem(caption, on_activate=None, submenu=None, sensitive=None, show=True, menu=None, item_class=gtk.MenuItem, active=False):
2310 weak_activate = None if (on_activate is None) else weakref.ref(on_activate) 2311 def do_weak_activate(item): 2312 act = weak_activate and weak_activate() 2313 if act: 2314 act(item)
2315 if ((caption is None) or (caption is '-')) and (on_activate is None) and (submenu is None): 2316 item = gtk.SeparatorMenuItem() 2317 else: 2318 item = item_class(caption) 2319 if active: 2320 item.set_active(True) 2321 if not (on_activate is None): 2322 item.connect('activate', do_weak_activate) 2323 if not (submenu is None): 2324 item.set_submenu(submenu) 2325 if (not (sensitive is None)) and (not (on_activate or submenu)) and (not sensitive): 2326 item.set_sensitive(False) 2327 if show: 2328 item.show() 2329 if menu: 2330 menu.append(item) 2331 return item 2332 2333
2334 -def is_front_app():
2335 for window in gtk.window_list_toplevels(): 2336 if window.is_active(): 2337 return True 2338 return False
2339 2340
2341 -def set_font_size(fs):
2342 Settings=gtk.settings_get_for_screen(gdk.screen_get_default()) 2343 gtk.rc_parse_string(""" 2344 style "general-font" 2345 { 2346 font_name = "Sans %s" 2347 } 2348 widget_class "*" style "general-font" """ % fs) 2349 gtk.rc_reset_styles(Settings)
2350 2351
2352 -def gtk_literally(s):
2353 # preserve underscores onscreen 2354 return s.replace('_', '__')
2355 2356
2357 -def get_active_window():
2358 for w in gtk.window_list_toplevels(): 2359 if w.is_active(): 2360 return w 2361 return None
2362 2363
2364 -class HyperTextView(gtk.TextView):
2365 __explore_featured = ['get_buffer', 'tag', 'links', 'write', 'write_link']
2366 - def __init__(self, *args, **kw):
2367 gtk.TextView.__init__(self, *args, **kw) 2368 self.set_wrap_mode(gtk.WRAP_WORD) 2369 tb = self.get_buffer() 2370 self.tag = tb.create_tag("blue", foreground='blue', underline=pango.UNDERLINE_SINGLE) 2371 self.tag.connect("event", self.__on_tag_event) 2372 self.links = collections.defaultdict(WeakCall)
2373 - def __on_tag_event(self, tag, widget, event, iter):
2374 if event.type == gtk.gdk.BUTTON_RELEASE: 2375 txt = read_tagged(self.get_buffer(), iter, self.tag) 2376 self.links[txt](txt)
2377 - def write(self, s):
2378 """Appends str(s) to the end of the buffer.""" 2379 tb = self.get_buffer() 2380 tb.insert(tb.get_end_iter(), str(s))
2393 2394
2395 -def read_tagged(buf, iter, tag):
2396 iter.backward_to_tag_toggle(tag) 2397 start = iter.copy() 2398 iter.forward_to_tag_toggle(tag) 2399 return buf.get_text(start, iter)
2400
2401 -class HyperTextView2(gtk.TextView):
2402 __explore_featured = ['get_buffer', 'tag', 'links', 'write', 'write_link']
2403 - def __init__(self, *args, **kw):
2404 gtk.TextView.__init__(self, *args, **kw) 2405 self.set_editable(False) 2406 self.set_wrap_mode(gtk.WRAP_WORD) 2407 tb = self.get_buffer() 2408 self.tag = tb.create_tag("blue", foreground='blue', underline=pango.UNDERLINE_SINGLE) 2409 self.tag.connect("event", self.__on_tag_event) 2410 self.links = collections.defaultdict(WeakCall)
2411 - def clear(self):
2412 self.get_buffer().set_text("") 2413 self.links = collections.defaultdict(WeakCall)
2414 - def __on_tag_event(self, tag, widget, event, iter):
2415 if event.type == gtk.gdk.BUTTON_RELEASE: 2416 txt = read_tagged(self.get_buffer(), iter, self.tag) 2417 iter.backward_to_tag_toggle(tag) 2418 self.links[iter.get_offset()]()
2419 - def write(self, *args):
2420 """Appends string(s) to the end of the buffer. 2421 If an argument is callable, it is used as the on_click handler for the following (string) argument which becomes a link. 2422 """ 2423 tb = self.get_buffer() 2424 on_click = None 2425 for arg in args: 2426 if (on_click is None) and callable(arg): 2427 on_click = arg 2428 elif not (on_click is None): 2429 self.write_link(arg, on_click) 2430 on_click = None 2431 else: 2432 tb.insert(tb.get_end_iter(), str(arg))
2443 2444 2445 2446 if __name__ == '__main__': 2447 from qubx.accept import * 2448 from math import * 2449 from itertools import * 2450 from random import * 2451 #d = FunctionArgsDialog(['x']) 2452 #d.run('broken') 2453 #print 'f(%s) = %s' % (', '.join(d.args), d.expr) 2454 #d = IterExpressionDialog() 2455 #if gtk.RESPONSE_ACCEPT == d.run('count()'): 2456 # print d.expr 2457 #d.destroy() 2458