Package conduit :: Package gtkui :: Module Canvas
[hide private]

Source Code for Module conduit.gtkui.Canvas

   1  """ 
   2  Manages adding, removing, resizing and drawing the canvas 
   3   
   4  The Canvas is the main area in Conduit, the area to which DataProviders are  
   5  dragged onto. 
   6   
   7  Copyright: John Stowers, 2006 
   8  License: GPLv2 
   9  """ 
  10  import cairo 
  11  import goocanvas 
  12  import gtk 
  13  import pango 
  14  from gettext import gettext as _ 
  15   
  16  import logging 
  17  log = logging.getLogger("gtkui.Canvas") 
  18   
  19  import conduit.utils as Utils 
  20  import conduit.Conduit as Conduit 
  21  import conduit.gtkui.Tree 
  22  import conduit.gtkui.Util as GtkUtil 
  23  import conduit.gtkui.Hints as Hints 
  24   
  25  log.info("Module Information: %s" % Utils.get_module_information(goocanvas, "pygoocanvas_version")) 
  26   
27 -class _StyleMixin:
28
29 - def _get_colors_and_state(self, styleName, stateName):
30 style = self.get_gtk_style() 31 if style: 32 colors = getattr(style, styleName.lower(), None) 33 state = getattr(gtk, "STATE_%s" % stateName.upper(), None) 34 else: 35 colors = None 36 state = None 37 38 return colors,state
39
40 - def get_gtk_style(self):
41 """ 42 @returns: The gtk.Style for the widget 43 """ 44 #not that clean, we can be mixed into the 45 #canvas, or a canvas item 46 try: 47 return self.get_canvas().style 48 except AttributeError: 49 try: 50 return self.style 51 except AttributeError: 52 return None
53
54 - def get_style_color_rgb(self, styleName, stateName):
55 colors,state = self._get_colors_and_state(styleName, stateName) 56 if colors != None and state != None: 57 return GtkUtil.gdk2rgb(colors[state]) 58 else: 59 return GtkUtil.gdk2rgb(GtkUtil.str2gdk("red"))
60
61 - def get_style_color_rgba(self, styleName, stateName, a=1):
62 colors,state = self._get_colors_and_state(styleName, stateName) 63 if colors != None and state != None: 64 return GtkUtil.gdk2rgba(colors[state], a) 65 else: 66 return GtkUtil.gdk2rgba(GtkUtil.str2gdk("red"), a)
67
68 - def get_style_color_int_rgb(self, styleName, stateName):
69 colors,state = self._get_colors_and_state(styleName, stateName) 70 if colors != None and state != None: 71 return GtkUtil.gdk2intrgb(colors[state]) 72 else: 73 return GtkUtil.gdk2intrgb(GtkUtil.str2gdk("red"))
74
75 - def get_style_color_int_rgba(self, styleName, stateName, a=1):
76 colors,state = self._get_colors_and_state(styleName, stateName) 77 if colors != None and state != None: 78 return GtkUtil.gdk2intrgba(colors[state], int(a*255)) 79 else: 80 return GtkUtil.gdk2intrgba(GtkUtil.str2gdk("red"), int(a*255))
81
82 -class _CanvasItem(goocanvas.Group, _StyleMixin):
83 84 #attributes common to Conduit and Dataprovider items 85 RECTANGLE_RADIUS = 4.0 86
87 - def __init__(self, parent, model):
88 #FIXME: If parent is None in base constructor then goocanvas segfaults 89 #this means a ref to items may be kept so this may leak... 90 goocanvas.Group.__init__(self, parent=parent) 91 self.model = model 92 93 #this little piece of magic re-applies style properties to the 94 #widgets, when the users theme changes 95 canv = self.get_canvas() 96 if canv: 97 canv.connect("style-set", self._automatic_style_updater)
98
99 - def _automatic_style_updater(self, *args):
100 if not self.get_gtk_style(): 101 #while in the midst of changing theme, the style is sometimes 102 #None, but dont worry, we will get called again 103 return 104 for attr in self.get_styled_item_names(): 105 item = getattr(self, attr, None) 106 if item: 107 item.set_properties( 108 **self.get_style_properties(attr) 109 )
110
111 - def get_height(self):
112 b = self.get_bounds() 113 return b.y2-b.y1
114
115 - def get_width(self):
116 b = self.get_bounds() 117 return b.x2-b.x1
118
119 - def get_top(self):
120 b = self.get_bounds() 121 return b.y1
122
123 - def get_bottom(self):
124 b = self.get_bounds() 125 return b.y2
126
127 - def get_left(self):
128 b = self.get_bounds() 129 return b.x1
130
131 - def get_right(self):
132 b = self.get_bounds() 133 return b.x2
134
135 - def get_styled_item_names(self):
136 raise NotImplementedError
137
138 - def get_style_properties(self, specifier):
139 raise NotImplementedError
140
141 -class Canvas(goocanvas.Canvas, _StyleMixin):
142 """ 143 This class manages many objects 144 """ 145 DND_TARGETS = [ 146 ('conduit/element-name', 0, 0) 147 ] 148 149 WELCOME_MESSAGE = _("Drag a Data Provider here to continue")
150 - def __init__(self, parentWindow, typeConverter, syncManager, dataproviderMenu, conduitMenu, msg):
151 """ 152 Draws an empty canvas of the appropriate size 153 """ 154 #setup the canvas 155 goocanvas.Canvas.__init__(self) 156 self.set_bounds(0, 0, 157 conduit.GLOBALS.settings.get("gui_initial_canvas_width"), 158 conduit.GLOBALS.settings.get("gui_initial_canvas_height") 159 ) 160 self.set_size_request( 161 conduit.GLOBALS.settings.get("gui_initial_canvas_width"), 162 conduit.GLOBALS.settings.get("gui_initial_canvas_height") 163 ) 164 self.root = self.get_root_item() 165 166 self.sync_manager = syncManager 167 self.typeConverter = typeConverter 168 self.parentWindow = parentWindow 169 self.msg = msg 170 171 self._setup_popup_menus(dataproviderMenu, conduitMenu) 172 173 #set up DND from the treeview 174 self.drag_dest_set( gtk.gdk.BUTTON1_MASK | gtk.gdk.BUTTON3_MASK, 175 self.DND_TARGETS, 176 gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_LINK) 177 self.connect('drag-motion', self.on_drag_motion) 178 self.connect('size-allocate', self._canvas_resized) 179 180 #track theme chages for canvas background 181 self.connect('realize', self._update_for_theme) 182 #We need a flag becuase otherwise we recurse forever. 183 #It appears that setting background_color_rgb in the 184 #sync-set handler causes sync-set to be emitted again, and again... 185 self._changing_style = False 186 self.connect("style-set", self._update_for_theme) 187 188 #keeps a reference to the currently selected (most recently clicked) 189 #canvas items 190 self.selectedConduitItem = None 191 self.selectedDataproviderItem = None 192 193 #model is a SyncSet, not set till later because it is loaded from xml 194 self.model = None 195 196 #Show a friendly welcome message on the canvas the first time the 197 #application is launched 198 self.welcome = None 199 self._maybe_show_welcome()
200
201 - def _do_hint(self, msgarea, respid):
202 if respid == Hints.BLANK_CANVAS: 203 new = conduit.GLOBALS.moduleManager.get_module_wrapper_with_instance("FolderTwoWay") 204 self.add_dataprovider_to_canvas( 205 "FolderTwoWay", 206 new, 207 1,1 208 )
209
210 - def _make_hint(self, hint, timeout=4):
211 if Hints.HINT_TEXT[hint][2]: 212 buttons = [("Show me",hint)] 213 else: 214 buttons = [] 215 h = self.msg.new_from_text_and_icon( 216 gtk.STOCK_INFO, 217 Hints.HINT_TEXT[hint][0], 218 Hints.HINT_TEXT[hint][1], 219 buttons=buttons, 220 timeout=timeout) 221 h.connect("response", self._do_hint) 222 h.show_all()
223
224 - def _show_hint(self, conduitCanvasItem, dataproviderCanvasItem, newItem):
225 if not self.msg: 226 return 227 228 if not conduit.GLOBALS.settings.get("gui_show_hints"): 229 return 230 231 if newItem == conduitCanvasItem: 232 self._make_hint(Hints.ADD_DATAPROVIDER) 233 elif newItem == dataproviderCanvasItem: 234 #check if we have a source and a sink 235 if conduitCanvasItem.model.can_sync(): 236 self._make_hint(Hints.RIGHT_CLICK_CONFIGURE)
237
238 - def _update_for_theme(self, *args):
239 if not self.get_gtk_style() or self._changing_style: 240 return 241 242 self._changing_style = True 243 self.set_property( 244 "background_color_rgb", 245 self.get_style_color_int_rgb("bg","normal") 246 ) 247 if self.welcome: 248 self.welcome.set_property( 249 "fill_color_rgba", 250 self.get_style_color_int_rgba("text","normal") 251 ) 252 self._changing_style = False
253
254 - def _setup_popup_menus(self, dataproviderPopupXML, conduitPopupXML):
255 """ 256 Sets up the popup menus and their callbacks 257 258 @param conduitPopupXML: The menu which is popped up when the user right 259 clicks on a conduit 260 @type conduitPopupXML: C{gtk.glade.XML} 261 @param dataproviderPopupXML: The menu which is popped up when the user right 262 clicks on a dataprovider 263 @type dataproviderPopupXML: C{gtk.glade.XML} 264 """ 265 266 self.dataproviderMenu = dataproviderPopupXML.get_widget("DataProviderMenu") 267 self.configureMenuItem = dataproviderPopupXML.get_widget("configure") 268 269 self.conduitMenu = conduitPopupXML.get_widget("ConduitMenu") 270 self.twoWayMenuItem = conduitPopupXML.get_widget("two_way_sync") 271 self.slowSyncMenuItem = conduitPopupXML.get_widget("slow_sync") 272 self.autoSyncMenuItem = conduitPopupXML.get_widget("auto_sync") 273 274 self.twoWayMenuItem.connect("toggled", self.on_two_way_sync_toggle) 275 self.slowSyncMenuItem.connect("toggled", self.on_slow_sync_toggle) 276 self.autoSyncMenuItem.connect("toggled", self.on_auto_sync_toggle) 277 278 #connect the conflict popups 279 self.policyWidgets = {} 280 for policyName in Conduit.CONFLICT_POLICY_NAMES: 281 for policyValue in Conduit.CONFLICT_POLICY_VALUES: 282 widgetName = "%s_%s" % (policyName,policyValue) 283 #store the widget and connect to toggled signal 284 widget = conduitPopupXML.get_widget(widgetName) 285 widget.connect("toggled", self.on_policy_toggle, policyName, policyValue) 286 self.policyWidgets[widgetName] = widget 287 288 #connect the menu callbacks 289 conduitPopupXML.signal_autoconnect(self) 290 dataproviderPopupXML.signal_autoconnect(self)
291
292 - def _delete_welcome(self):
293 idx = self.root.find_child(self.welcome) 294 if idx != -1: 295 self.root.remove_child(idx) 296 self.welcome = None
297
298 - def _create_welcome(self):
299 c_x,c_y,c_w,c_h = self.get_bounds() 300 self.welcome = ConduitCanvasItem( 301 parent=self.root, 302 model=None, 303 width=c_w 304 )
305
306 - def _maybe_show_welcome(self):
307 """ 308 Adds a friendly welcome to the canvas. Only does so only if 309 there are no conduits, otherwise it would just get in the way. 310 """ 311 if self.model == None or (self.model != None and self.model.num_conduits() == 0): 312 if self.welcome == None: 313 self._create_welcome() 314 if self.msg and conduit.GLOBALS.settings.get("gui_show_hints"): 315 self._make_hint(Hints.BLANK_CANVAS, timeout=0) 316 317 elif self.welcome: 318 self._delete_welcome()
319
321 items = [] 322 for i in range(0, self.root.get_n_children()): 323 condItem = self.root.get_child(i) 324 if isinstance(condItem, ConduitCanvasItem): 325 if condItem != self.welcome: 326 items.append(condItem) 327 return items
328
330 items = [] 331 for c in self._get_child_conduit_canvas_items(): 332 for i in range(0, c.get_n_children()): 333 dpItem = c.get_child(i) 334 if isinstance(dpItem, DataProviderCanvasItem): 335 items.append(dpItem) 336 return items
337
338 - def _canvas_resized(self, widget, allocation):
339 self.set_bounds( 340 0,0, 341 allocation.width, 342 self._get_minimum_canvas_size(allocation.height) 343 ) 344 for i in self._get_child_conduit_canvas_items(): 345 i.set_width(allocation.width)
346
347 - def _on_conduit_button_press(self, view, target, event):
348 """ 349 Handle button clicks on conduits 350 """ 351 self.selectedConduitItem = view 352 353 #right click 354 if event.type == gtk.gdk.BUTTON_PRESS: 355 if event.button == 3: 356 #Preset the two way menu items sensitivity 357 if not self.selectedConduitItem.model.can_do_two_way_sync(): 358 self.twoWayMenuItem.set_property("sensitive", False) 359 else: 360 self.twoWayMenuItem.set_property("sensitive", True) 361 #Set item ticked if two way sync enabled 362 self.twoWayMenuItem.set_active(self.selectedConduitItem.model.is_two_way()) 363 #Set item ticked if two way sync enabled 364 self.slowSyncMenuItem.set_active(self.selectedConduitItem.model.slowSyncEnabled) 365 #Set item ticked if two way sync enabled 366 self.autoSyncMenuItem.set_active(self.selectedConduitItem.model.autoSyncEnabled) 367 #Set the conflict and delete policy 368 for policyName in Conduit.CONFLICT_POLICY_NAMES: 369 policyValue = self.selectedConduitItem.model.get_policy(policyName) 370 widgetName = "%s_%s" % (policyName,policyValue) 371 self.policyWidgets[widgetName].set_active(True) 372 373 #Show the menu 374 if not self.selectedConduitItem.model.is_busy(): 375 self.conduitMenu.popup( 376 None, None, 377 None, event.button, event.time 378 ) 379 #dont propogate the event 380 return True
381
382 - def _on_dataprovider_button_press(self, view, target, event):
383 """ 384 Handle button clicks 385 386 @param user_data_dataprovider_wrapper: The dpw that was clicked 387 @type user_data_dataprovider_wrapper: L{conduit.Module.ModuleWrapper} 388 """ 389 self.selectedDataproviderItem = view 390 self.selectedConduitItem = view.get_parent() 391 392 #single right click 393 if event.type == gtk.gdk.BUTTON_PRESS: 394 if event.button == 3: 395 if view.model.enabled and not view.model.module.is_busy(): 396 self.configureMenuItem.set_property("sensitive", view.model.configurable) 397 #show the menu 398 self.dataproviderMenu.popup( 399 None, None, 400 None, event.button, event.time 401 ) 402 403 #double left click 404 elif event.type == gtk.gdk._2BUTTON_PRESS: 405 if event.button == 1: 406 if view.model.enabled and not view.model.module.is_busy(): 407 if view.model.configurable: 408 self.on_configure_dataprovider_clicked(None) 409 410 #dont propogate the event 411 return True
412
414 """ 415 Gets the Y coordinate at the bottom of all visible conduits 416 417 @returns: A coordinate (postivive down) from the canvas origin 418 @rtype: C{int} 419 """ 420 y = 0.0 421 for i in self._get_child_conduit_canvas_items(): 422 y = y + i.get_height() 423 return y
424
425 - def _get_minimum_canvas_size(self, allocH=None):
426 if not allocH: 427 allocH = self.get_allocation().height 428 429 bottom = self._get_bottom_of_conduits_coord() 430 return max(bottom + ConduitCanvasItem.WIDGET_HEIGHT + 20, allocH)
431
432 - def _remove_overlap(self):
433 """ 434 Moves the ConduitCanvasItems to stop them overlapping visually 435 """ 436 items = self._get_child_conduit_canvas_items() 437 if len(items) > 0: 438 #special case where the top one was deleted 439 top = items[0].get_top()-(items[0].LINE_WIDTH/2) 440 if top != 0.0: 441 for item in items: 442 #translate all those below 443 item.translate(0,-top) 444 else: 445 for i in xrange(0, len(items)): 446 try: 447 overlap = items[i].get_bottom() - items[i+1].get_top() 448 if overlap != 0.0: 449 #translate all those below 450 for item in items[i+1:]: 451 item.translate(0,overlap) 452 except IndexError: 453 break
454
455 - def on_conduit_removed(self, sender, conduitRemoved):
456 for item in self._get_child_conduit_canvas_items(): 457 if item.model == conduitRemoved: 458 #remove the canvas item 459 idx = self.root.find_child(item) 460 if idx != -1: 461 self.root.remove_child(idx) 462 else: 463 log.warn("Error finding item") 464 self._remove_overlap() 465 466 self._maybe_show_welcome() 467 c_x,c_y,c_w,c_h = self.get_bounds() 468 self.set_bounds( 469 0, 470 0, 471 c_w, 472 self._get_minimum_canvas_size() 473 )
474
475 - def on_conduit_added(self, sender, conduitAdded):
476 """ 477 Creates a ConduitCanvasItem for the new conduit 478 """ 479 #check for duplicates to eliminate race condition in set_sync_set 480 if conduitAdded in [i.model for i in self._get_child_conduit_canvas_items()]: 481 return 482 483 c_x,c_y,c_w,c_h = self.get_bounds() 484 #Create the item and move it into position 485 bottom = self._get_bottom_of_conduits_coord() 486 conduitCanvasItem = ConduitCanvasItem( 487 parent=self.root, 488 model=conduitAdded, 489 width=c_w) 490 conduitCanvasItem.connect('button-press-event', self._on_conduit_button_press) 491 conduitCanvasItem.translate( 492 conduitCanvasItem.LINE_WIDTH/2.0, 493 bottom+(conduitCanvasItem.LINE_WIDTH/2.0) 494 ) 495 496 for dp in conduitAdded.get_all_dataproviders(): 497 self.on_dataprovider_added(None, dp, conduitCanvasItem) 498 499 conduitAdded.connect("dataprovider-added", self.on_dataprovider_added, conduitCanvasItem) 500 conduitAdded.connect("dataprovider-removed", self.on_dataprovider_removed, conduitCanvasItem) 501 502 self._maybe_show_welcome() 503 self.set_bounds( 504 0, 505 0, 506 c_w, 507 self._get_minimum_canvas_size() 508 ) 509 510 self._show_hint(conduitCanvasItem, None, conduitCanvasItem)
511
512 - def on_dataprovider_removed(self, sender, dataproviderRemoved, conduitCanvasItem):
513 for item in self._get_child_dataprovider_canvas_items(): 514 if item.model == dataproviderRemoved: 515 conduitCanvasItem.delete_dataprovider_canvas_item(item) 516 self._remove_overlap()
517
518 - def on_dataprovider_added(self, sender, dataproviderAdded, conduitCanvasItem):
519 """ 520 Creates a DataProviderCanvasItem for the new dataprovider and adds it to 521 the canvas 522 """ 523 524 #check for duplicates to eliminate race condition in set_sync_set 525 if dataproviderAdded in [i.model for i in self._get_child_dataprovider_canvas_items()]: 526 return 527 528 item = DataProviderCanvasItem( 529 parent=conduitCanvasItem, 530 model=dataproviderAdded 531 ) 532 item.connect('button-press-event', self._on_dataprovider_button_press) 533 conduitCanvasItem.add_dataprovider_canvas_item(item) 534 self._remove_overlap() 535 536 self._show_hint(conduitCanvasItem, item, item)
537
538 - def get_sync_set(self):
539 return self.model
540
541 - def set_sync_set(self, syncSet):
542 self.model = syncSet 543 for c in self.model.get_all_conduits(): 544 self.on_conduit_added(None, c) 545 546 self.model.connect("conduit-added", self.on_conduit_added) 547 self.model.connect("conduit-removed", self.on_conduit_removed)
548
549 - def on_drag_motion(self, wid, context, x, y, time):
550 context.drag_status(gtk.gdk.ACTION_COPY, time) 551 return True
552
553 - def on_delete_conduit_clicked(self, widget):
554 """ 555 Delete a conduit and all its associated dataproviders 556 """ 557 conduitCanvasItem = self.selectedConduitItem 558 cond = conduitCanvasItem.model 559 self.model.remove_conduit(cond)
560
561 - def on_refresh_conduit_clicked(self, widget):
562 """ 563 Refresh the selected conduit 564 """ 565 self.selectedConduitItem.model.refresh()
566
567 - def on_synchronize_conduit_clicked(self, widget):
568 """ 569 Synchronize the selected conduit 570 """ 571 self.selectedConduitItem.model.sync()
572
573 - def on_delete_dataprovider_clicked(self, widget):
574 """ 575 Delete the selected dataprovider 576 """ 577 dp = self.selectedDataproviderItem.model 578 conduitCanvasItem = self.selectedDataproviderItem.get_parent() 579 cond = conduitCanvasItem.model 580 cond.delete_dataprovider(dp)
581
582 - def on_configure_dataprovider_clicked(self, widget):
583 """ 584 Calls the configure method on the selected dataprovider 585 """ 586 dp = self.selectedDataproviderItem.model.module 587 log.info("Configuring %s" % dp) 588 #May block 589 dp.configure(self.parentWindow) 590 self.selectedDataproviderItem.update_appearance()
591
592 - def on_refresh_dataprovider_clicked(self, widget):
593 """ 594 Refreshes a single dataprovider 595 """ 596 dp = self.selectedDataproviderItem.model 597 #dp.module.refresh() 598 cond = self.selectedConduitItem.model 599 cond.refresh_dataprovider(dp)
600
601 - def on_two_way_sync_toggle(self, widget):
602 """ 603 Enables or disables two way sync on dataproviders. 604 """ 605 if widget.get_active(): 606 self.selectedConduitItem.model.enable_two_way_sync() 607 else: 608 self.selectedConduitItem.model.disable_two_way_sync()
609
610 - def on_slow_sync_toggle(self, widget):
611 """ 612 Enables or disables slow sync of dataproviders. 613 """ 614 if widget.get_active(): 615 self.selectedConduitItem.model.enable_slow_sync() 616 else: 617 self.selectedConduitItem.model.disable_slow_sync()
618
619 - def on_auto_sync_toggle(self, widget):
620 """ 621 Enables or disables slow sync of dataproviders. 622 """ 623 if widget.get_active(): 624 self.selectedConduitItem.model.enable_auto_sync() 625 else: 626 self.selectedConduitItem.model.disable_auto_sync()
627
628 - def on_policy_toggle(self, widget, policyName, policyValue):
629 if widget.get_active(): 630 self.selectedConduitItem.model.set_policy(policyName, policyValue)
631
632 - def add_dataprovider_to_canvas(self, key, dataproviderWrapper, x, y):
633 """ 634 Adds a new dataprovider to the Canvas 635 636 @param module: The dataprovider wrapper to add to the canvas 637 @type module: L{conduit.Module.ModuleWrapper}. 638 @param x: The x location on the canvas to place the module widget 639 @type x: C{int} 640 @param y: The y location on the canvas to place the module widget 641 @type y: C{int} 642 @returns: The conduit that the dataprovider was added to 643 """ 644 parent = None 645 existing = self.get_item_at(x,y,False) 646 c_x,c_y,c_w,c_h = self.get_bounds() 647 648 #if the user dropped on the right half of the canvas try add into the sink position 649 if x < (c_w/2): 650 trySourceFirst = True 651 else: 652 trySourceFirst = False 653 654 #recurse up the canvas objects to determine if we have been dropped 655 #inside an existing conduit 656 if existing: 657 parent = existing.get_parent() 658 while parent != None and not parent == self.welcome and not isinstance(parent, ConduitCanvasItem): 659 parent = parent.get_parent() 660 661 #if we were dropped on the welcome message we first remove that 662 if parent and parent == self.welcome: 663 self._delete_welcome() 664 #ensure a new conduit is created 665 parent = None 666 667 if parent != None: 668 #we were dropped on an existing conduit 669 parent.model.add_dataprovider(dataproviderWrapper, trySourceFirst) 670 return 671 672 #create a new conduit 673 cond = Conduit.Conduit(self.sync_manager) 674 cond.add_dataprovider(dataproviderWrapper, trySourceFirst) 675 self.model.add_conduit(cond)
676
677 - def clear_canvas(self):
678 self.model.clear()
679
680 -class DataProviderCanvasItem(_CanvasItem):
681 682 WIDGET_WIDTH = 130 683 WIDGET_HEIGHT = 50 684 IMAGE_TO_TEXT_PADDING = 5 685 PENDING_MESSAGE = "Pending" 686 MAX_TEXT_LENGTH = 10 687 MAX_TEXT_LINES = 2 688 LINE_WIDTH = 2.0 689
690 - def __init__(self, parent, model):
691 _CanvasItem.__init__(self, parent, model) 692 693 self._build_widget() 694 self.set_model(model)
695
696 - def _get_model_name(self):
697 #FIXME: Goocanvas.Text does not ellipsize text, 698 #so we do it...... poorly 699 text = "" 700 lines = 1 701 for word in self.model.get_name().split(" "): 702 if len(word) > self.MAX_TEXT_LENGTH: 703 word = word[0:self.MAX_TEXT_LENGTH] + "... " 704 else: 705 word = word + " " 706 707 #gross guess for how much of the space we have used 708 if (len(text)+len(word)) > (self.MAX_TEXT_LENGTH*self.MAX_TEXT_LINES): 709 #append final elipsis 710 if not word.endswith("... "): 711 text = text + "..." 712 break 713 else: 714 text = text + word 715 716 return text
717
718 - def _get_icon(self):
719 return self.model.get_icon()
720
721 - def _build_widget(self):
722 self.box = goocanvas.Rect( 723 x=0, 724 y=0, 725 width=self.WIDGET_WIDTH-(2*self.LINE_WIDTH), 726 height=self.WIDGET_HEIGHT-(2*self.LINE_WIDTH), 727 radius_y=self.RECTANGLE_RADIUS, 728 radius_x=self.RECTANGLE_RADIUS, 729 **self.get_style_properties("box") 730 ) 731 pb = self.model.get_icon() 732 pbx = int((1*self.WIDGET_WIDTH/5) - (pb.get_width()/2)) 733 pby = int((1*self.WIDGET_HEIGHT/3) - (pb.get_height()/2)) 734 self.image = goocanvas.Image(pixbuf=pb, 735 x=pbx, 736 y=pby 737 ) 738 self.name = goocanvas.Text( 739 x=pbx + pb.get_width() + self.IMAGE_TO_TEXT_PADDING, 740 y=int(1*self.WIDGET_HEIGHT/3), 741 width=3*self.WIDGET_WIDTH/5, 742 text=self._get_model_name(), 743 anchor=gtk.ANCHOR_WEST, 744 **self.get_style_properties("name") 745 ) 746 self.statusText = goocanvas.Text( 747 x=int(1*self.WIDGET_WIDTH/10), 748 y=int(2*self.WIDGET_HEIGHT/3), 749 width=4*self.WIDGET_WIDTH/5, 750 text="", 751 anchor=gtk.ANCHOR_WEST, 752 **self.get_style_properties("statusText") 753 ) 754 755 #Add all the visual elements which represent a dataprovider 756 self.add_child(self.box) 757 self.add_child(self.name) 758 self.add_child(self.image) 759 self.add_child(self.statusText)
760
761 - def _on_change_detected(self, dataprovider):
762 log.debug("CHANGE DETECTED")
763
764 - def _on_status_changed(self, dataprovider):
765 msg = dataprovider.get_status() 766 self.statusText.set_property("text", msg)
767
768 - def get_styled_item_names(self):
769 return "box","name","statusText"
770
771 - def get_style_properties(self, specifier):
772 if specifier == "box": 773 #color the box differently if it is pending, i.e. unavailable, 774 #disconnected, etc. 775 if self.model.module == None: 776 insensitive = self.get_style_color_int_rgba("mid","insensitive") 777 kwargs = { 778 "line_width":1.5, 779 "stroke_color_rgba":insensitive, 780 "fill_color_rgba":insensitive 781 } 782 783 else: 784 pattern = cairo.LinearGradient(0, 0, 0, 100) 785 pattern.add_color_stop_rgb( 786 0, 787 *self.get_style_color_rgb("dark","active") 788 ); 789 pattern.add_color_stop_rgb( 790 0.5, 791 *self.get_style_color_rgb("dark","prelight") 792 ); 793 794 kwargs = { 795 "line_width":2.0, 796 "stroke_color":"black", 797 "fill_pattern":pattern 798 } 799 elif specifier == "name": 800 kwargs = { 801 "font":"Sans 8", 802 "fill_color_rgba":self.get_style_color_int_rgba("text","normal") 803 } 804 elif specifier == "statusText": 805 kwargs = { 806 "font":"Sans 7", 807 "fill_color_rgba":self.get_style_color_int_rgba("text_aa","normal") 808 } 809 810 return kwargs
811
812 - def update_appearance(self):
813 #the image 814 pb = self._get_icon() 815 pbx = int((1*self.WIDGET_WIDTH/5) - (pb.get_width()/2)) 816 pby = int((1*self.WIDGET_HEIGHT/3) - (pb.get_height()/2)) 817 self.image.set_property("pixbuf",pb) 818 819 self.name.set_property("text",self._get_model_name()) 820 821 if self.model.module == None: 822 statusText = self.PENDING_MESSAGE 823 else: 824 statusText = self.model.module.get_status() 825 self.statusText.set_property("text",statusText) 826 827 self.box.set_properties( 828 **self.get_style_properties("box") 829 )
830
831 - def set_model(self, model):
832 self.model = model 833 self.update_appearance() 834 if self.model.module != None: 835 self.model.module.connect("change-detected", self._on_change_detected) 836 self.model.module.connect("status-changed", self._on_status_changed)
837
838 -class ConduitCanvasItem(_CanvasItem):
839 840 DIVIDER = False 841 FLAT_BOX = True 842 WIDGET_HEIGHT = 63.0 843 SIDE_PADDING = 10.0 844 LINE_WIDTH = 2.0 845
846 - def __init__(self, parent, model, width):
847 _CanvasItem.__init__(self, parent, model) 848 849 if self.model: 850 self.model.connect("parameters-changed", self._on_conduit_parameters_changed) 851 self.model.connect("dataprovider-changed", self._on_conduit_dataprovider_changed) 852 self.model.connect("sync-progress", self._on_conduit_progress) 853 854 self.sourceItem = None 855 self.sinkDpItems = [] 856 self.connectorItems = {} 857 858 self.l = None 859 self.progressText = None 860 self.boundingBox = None 861 862 #if self.DIVIDER, show an transparent bouding box, and a 863 #simple dividing line 864 self.divider = None 865 #goocanvas.Points need a list of tuples, not a list of lists. Yuck 866 self.dividerPoints = [(),()] 867 868 #Build the widget 869 self._build_widget(width)
870
871 - def _add_progress_text(self):
872 if self.sourceItem != None and len(self.sinkDpItems) > 0: 873 if self.progressText == None: 874 fromx,fromy,tox,toy = self._get_connector_coordinates(self.sourceItem,self.sinkDpItems[0]) 875 self.progressText = goocanvas.Text( 876 x=fromx+5, 877 y=fromy-15, 878 width=100, 879 text="", 880 anchor=gtk.ANCHOR_WEST, 881 alignment=pango.ALIGN_LEFT, 882 **self.get_style_properties("progressText") 883 ) 884 self.add_child(self.progressText)
885
886 - def _position_dataprovider(self, dpCanvasItem):
887 dpx, dpy = self.model.get_dataprovider_position(dpCanvasItem.model) 888 if dpx == 0: 889 #Its a source 890 dpCanvasItem.translate( 891 self.SIDE_PADDING, 892 self.SIDE_PADDING + self.l.get_property("line_width") 893 ) 894 else: 895 #Its a sink 896 if dpy == 0: 897 i = self.SIDE_PADDING 898 else: 899 i = (dpy * self.SIDE_PADDING) + self.SIDE_PADDING 900 901 dpCanvasItem.translate( 902 self.get_width() - dpCanvasItem.get_width() - self.SIDE_PADDING, 903 (dpy * dpCanvasItem.get_height()) + i + self.l.get_property("line_width") 904 )
905
906 - def _build_widget(self, width):
907 true_width = width-self.LINE_WIDTH 908 909 #draw a spacer to give some space between conduits 910 points = goocanvas.Points([(0.0, 0.0), (true_width, 0.0)]) 911 self.l = goocanvas.Polyline( 912 points=points, 913 line_width=self.LINE_WIDTH, 914 stroke_color_rgba=GtkUtil.TRANSPARENT_COLOR 915 ) 916 self.add_child(self.l) 917 918 #draw a box which will contain the dataproviders 919 self.boundingBox = goocanvas.Rect( 920 x=0, 921 y=5, 922 width=true_width, 923 height=self.WIDGET_HEIGHT, 924 radius_y=self.RECTANGLE_RADIUS, 925 radius_x=self.RECTANGLE_RADIUS, 926 **self.get_style_properties("boundingBox") 927 ) 928 self.add_child(self.boundingBox) 929 if self.DIVIDER: 930 #draw an underline 931 #from point 932 self.dividerPoints[0] = (true_width*0.33,5+self.WIDGET_HEIGHT) 933 self.dividerPoints[1] = (2*(true_width*0.33),5+self.WIDGET_HEIGHT) 934 935 self.divider = goocanvas.Polyline( 936 points=goocanvas.Points(self.dividerPoints), 937 **self.get_style_properties("divider") 938 ) 939 self.add_child(self.divider)
940
941 - def _resize_height(self):
942 sourceh = 0.0 943 sinkh = 0.0 944 padding = 0.0 945 for dpw in self.sinkDpItems: 946 sinkh += dpw.get_height() 947 #padding between items 948 numSinks = len(self.sinkDpItems) 949 if numSinks: 950 sinkh += ((numSinks - 1)*self.SIDE_PADDING) 951 if self.sourceItem != None: 952 sourceh += self.sourceItem.get_height() 953 954 self.set_height( 955 max(sourceh, sinkh)+ #expand to the largest 956 (1.5*self.SIDE_PADDING) #padding at the top and bottom 957 )
958
959 - def _delete_connector(self, item):
960 """ 961 Deletes the connector associated with the sink item 962 """ 963 try: 964 connector = self.connectorItems[item] 965 idx = self.find_child(connector) 966 if idx != -1: 967 self.remove_child(idx) 968 else: 969 log.warn("Could not find child connector item") 970 971 del(self.connectorItems[item]) 972 except KeyError: pass
973
974 - def _on_conduit_parameters_changed(self, cond):
975 self.update_appearance()
976
977 - def _on_conduit_dataprovider_changed(self, cond, olddpw, newdpw):
978 for item in [self.sourceItem] + self.sinkDpItems: 979 if item.model.get_key() == olddpw.get_key(): 980 item.set_model(newdpw)
981
982 - def _on_conduit_progress(self, cond, percent, UIDs):
983 self.progressText.set_property("text","%2.1d%% complete" % int(percent*100.0))
984
985 - def _get_connector_coordinates(self, fromdp, todp):
986 """ 987 Calculates the points a connector shall connect to between fromdp and todp 988 @returns: fromx,fromy,tox,toy 989 """ 990 fromx = fromdp.get_right() 991 fromy = fromdp.get_top() + (fromdp.get_height()/2) - self.get_top() 992 tox = todp.get_left() 993 toy = todp.get_top() + (todp.get_height()/2) - self.get_top() 994 return fromx,fromy,tox,toy
995
996 - def _remove_overlap(self):
997 items = self.sinkDpItems 998 if len(items) > 0: 999 #special case where the top one was deleted 1000 top = items[0].get_top()-self.get_top()-self.SIDE_PADDING-items[0].LINE_WIDTH 1001 if top != 0.0: 1002 for item in items: 1003 #translate all those below 1004 item.translate(0,-top) 1005 if self.sourceItem != None: 1006 fromx,fromy,tox,toy = self._get_connector_coordinates(self.sourceItem,item) 1007 self.connectorItems[item].reconnect(fromx,fromy,tox,toy) 1008 else: 1009 for i in xrange(0, len(items)): 1010 try: 1011 overlap = items[i].get_bottom() - items[i+1].get_top() 1012 log.debug("Sink Overlap: %s %s ----> %s" % (overlap,i,i+1)) 1013 #If there is anything more than the normal padding gap between then 1014 #the dp must be translated 1015 if overlap < -self.SIDE_PADDING: 1016 #translate all those below, and make their connectors work again 1017 for item in items[i+1:]: 1018 item.translate(0,overlap+self.SIDE_PADDING) 1019 if self.sourceItem != None: 1020 fromx,fromy,tox,toy = self._get_connector_coordinates(self.sourceItem,item) 1021 self.connectorItems[item].reconnect(fromx,fromy,tox,toy) 1022 except IndexError: 1023 break
1024
1025 - def get_styled_item_names(self):
1026 return "boundingBox","progressText","divider"
1027
1028 - def get_style_properties(self, specifier):
1029 if specifier == "boundingBox": 1030 if self.DIVIDER: 1031 kwargs = { 1032 "line_width":0 1033 } 1034 else: 1035 if self.FLAT_BOX: 1036 kwargs = { 1037 "line_width":0, 1038 "fill_color_rgba":self.get_style_color_int_rgba("base","prelight") 1039 } 1040 else: 1041 pattern = cairo.LinearGradient(0, -30, 0, 100) 1042 pattern.add_color_stop_rgb( 1043 0, 1044 *self.get_style_color_rgb("dark","selected") 1045 ); 1046 pattern.add_color_stop_rgb( 1047 0.7, 1048 *self.get_style_color_rgb("mid","selected") 1049 ); 1050 1051 kwargs = { 1052 "line_width":2.0, 1053 "fill_pattern":pattern, 1054 "stroke_color_rgba":self.get_style_color_int_rgba("text","normal") 1055 } 1056 1057 elif specifier == "progressText": 1058 kwargs = { 1059 "font":"Sans 7", 1060 "fill_color_rgba":self.get_style_color_int_rgba("text","normal") 1061 } 1062 elif specifier == "divider": 1063 kwargs = { 1064 "line_width":3.0, 1065 "line_cap":cairo.LINE_CAP_ROUND, 1066 "stroke_color_rgba":self.get_style_color_int_rgba("text_aa","normal") 1067 } 1068 else: 1069 kwargs = {} 1070 1071 return kwargs
1072
1073 - def update_appearance(self):
1074 self._resize_height() 1075 1076 #update the twowayness of the connectors 1077 for c in self.connectorItems.values(): 1078 c.set_two_way(self.model.is_two_way())
1079
1080 - def add_dataprovider_canvas_item(self, item):
1081 self._position_dataprovider(item) 1082 1083 #is it a sink or a source? 1084 dpx, dpy = self.model.get_dataprovider_position(item.model) 1085 if dpx == 0: 1086 self.sourceItem = item 1087 else: 1088 self.sinkDpItems.append(item) 1089 1090 #add a connector. If we just added a source then we need to make all the 1091 #connectors, otherwise we just need to add a connector for the new item 1092 if dpx == 0: 1093 #make all the connectors 1094 for s in self.sinkDpItems: 1095 fromx,fromy,tox,toy = self._get_connector_coordinates(self.sourceItem,s) 1096 c = ConnectorCanvasItem(self, 1097 fromx, 1098 fromy, 1099 tox, 1100 toy, 1101 self.model.is_two_way(), 1102 conduit.GLOBALS.typeConverter.conversion_exists( 1103 self.sourceItem.model.get_output_type(), 1104 s.model.get_input_type() 1105 ) 1106 ) 1107 self.connectorItems[s] = c 1108 else: 1109 #just make the new connector 1110 if self.sourceItem != None: 1111 fromx,fromy,tox,toy = self._get_connector_coordinates(self.sourceItem,item) 1112 c = ConnectorCanvasItem(self, 1113 fromx, 1114 fromy, 1115 tox, 1116 toy, 1117 self.model.is_two_way(), 1118 conduit.GLOBALS.typeConverter.conversion_exists( 1119 self.sourceItem.model.get_output_type(), 1120 item.model.get_input_type() 1121 ) 1122 ) 1123 self.connectorItems[item] = c 1124 1125 self._add_progress_text() 1126 self.update_appearance()
1127
1128 - def delete_dataprovider_canvas_item(self, item):
1129 """ 1130 Removes the DataProviderCanvasItem and its connectors 1131 """ 1132 idx = self.find_child(item) 1133 if idx != -1: 1134 self.remove_child(idx) 1135 else: 1136 log.warn("Could not find child dataprovider item") 1137 1138 if item == self.sourceItem: 1139 self.sourceItem = None 1140 #remove all connectors (copy because we modify in place) 1141 for item in self.connectorItems.copy(): 1142 self._delete_connector(item) 1143 else: 1144 self.sinkDpItems.remove(item) 1145 self._delete_connector(item) 1146 1147 self._remove_overlap() 1148 self.