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

Source Code for Module conduit.gtkui.ConflictResolver

  1  """ 
  2  Holds classes used for resolving conflicts. 
  3   
  4  Copyright: John Stowers, 2006 
  5  License: GPLv2 
  6  """ 
  7  import traceback 
  8  import time 
  9  import gobject 
 10  import gtk, gtk.gdk 
 11  import pango 
 12  import logging 
 13  log = logging.getLogger("gtkui.ConflictResolver") 
 14   
 15  import conduit 
 16  import conduit.dataproviders.DataProvider as DataProvider 
 17  import conduit.Vfs as Vfs 
 18  import conduit.Conflict as Conflict 
 19   
 20  from gettext import gettext as _ 
 21   
 22  #Indexes into the conflict tree model in which conflict data is stored 
 23  CONFLICT_IDX = 0            #The conflict object 
 24  DIRECTION_IDX = 1           #The current user decision re: the conflict (-->, <-- or -x-) 
 25   
26 -class ConflictHeader:
27 - def __init__(self, sourceWrapper, sinkWrapper):
28 self.sourceWrapper = sourceWrapper 29 self.sinkWrapper = sinkWrapper
30
31 - def get_snippet(self, is_source):
32 if is_source: 33 return self.sourceWrapper.name 34 else: 35 return self.sinkWrapper.name
36
37 - def get_icon(self, is_source):
38 if is_source: 39 return self.sourceWrapper.get_icon() 40 else: 41 return self.sinkWrapper.get_icon()
42
43 -class ConflictResolver:
44 """ 45 Manages a gtk.TreeView which is used for asking the user what they 46 wish to do in the case of a conflict 47 """
48 - def __init__(self, widgets):
49 self.model = gtk.TreeStore( gobject.TYPE_PYOBJECT, #Conflict 50 gobject.TYPE_INT #Resolved direction 51 ) 52 #In the conflict treeview, group by sink <-> source partnership 53 self.partnerships = {} 54 self.numConflicts = 0 55 56 self.view = gtk.TreeView( self.model ) 57 self._build_view() 58 59 #Connect up the GUI 60 #this is the scrolled window in the bottom of the main gui 61 self.expander = widgets.get_widget("conflictExpander") 62 self.expander.connect("activate", self.on_expand) 63 self.vpane = widgets.get_widget("vpaned1") 64 self.expander.set_sensitive(False) 65 self.fullscreenButton = widgets.get_widget("conflictFullscreenButton") 66 self.fullscreenButton.connect("toggled", self.on_fullscreen_toggled) 67 self.conflictScrolledWindow = widgets.get_widget("conflictExpanderVBox") 68 widgets.get_widget("conflictScrolledWindow").add(self.view) 69 #this is a stand alone window for showing conflicts in an easier manner 70 self.standalone = gtk.Window() 71 self.standalone.set_title("Conflicts") 72 self.standalone.set_transient_for(widgets.get_widget("MainWindow")) 73 self.standalone.set_position(gtk.WIN_POS_CENTER_ON_PARENT) 74 self.standalone.set_destroy_with_parent(True) 75 self.standalone.set_default_size(-1, 200) 76 #widgets cannot have two parents 77 #self.standalone.add(self.conflictScrolledWindow) 78 self.standalone.connect("delete-event", self.on_standalone_closed) 79 #the button callbacks are shared 80 widgets.get_widget("conflictCancelButton").connect("clicked", self.on_cancel_conflicts) 81 widgets.get_widget("conflictResolveButton").connect("clicked", self.on_resolve_conflicts) 82 #the state of the compare button is managed by the selection changed callback 83 self.compareButton = widgets.get_widget("conflictCompareButton") 84 self.compareButton.connect("clicked", self.on_compare_conflicts) 85 self.compareButton.set_sensitive(False)
86
87 - def _build_view(self):
88 #Visible column0 is 89 #[pixbuf + source display name] or 90 #[source_data.get_snippet()] 91 column0 = gtk.TreeViewColumn(_("Source")) 92 93 sourceIconRenderer = gtk.CellRendererPixbuf() 94 sourceNameRenderer = gtk.CellRendererText() 95 sourceNameRenderer.set_property('ellipsize', pango.ELLIPSIZE_END) 96 column0.pack_start(sourceIconRenderer, False) 97 column0.pack_start(sourceNameRenderer, True) 98 99 column0.set_property("expand", True) 100 column0.set_cell_data_func(sourceNameRenderer, self._name_data_func, True) 101 column0.set_cell_data_func(sourceIconRenderer, self._icon_data_func, True) 102 103 #Visible column1 is the arrow to decide the direction 104 confRenderer = ConflictCellRenderer() 105 column1 = gtk.TreeViewColumn(_("Resolution"), confRenderer) 106 column1.set_cell_data_func(confRenderer, self._direction_data_func, DIRECTION_IDX) 107 column1.set_property("expand", False) 108 109 #Visible column2 is the display name of source and source data 110 column2 = gtk.TreeViewColumn(_("Sink")) 111 112 sinkIconRenderer = gtk.CellRendererPixbuf() 113 sinkNameRenderer = gtk.CellRendererText() 114 sinkNameRenderer.set_property('ellipsize', pango.ELLIPSIZE_END) 115 column2.pack_start(sinkIconRenderer, False) 116 column2.pack_start(sinkNameRenderer, True) 117 118 column2.set_property("expand", True) 119 column2.set_cell_data_func(sinkNameRenderer, self._name_data_func, False) 120 column2.set_cell_data_func(sinkIconRenderer, self._icon_data_func, False) 121 122 for c in [column0,column1,column2]: 123 self.view.append_column( c ) 124 125 #set view properties 126 self.view.set_property("enable-search", False) 127 self.view.get_selection().connect("changed", self.on_selection_changed)
128
129 - def _name_data_func(self, column, cell_renderer, tree_model, rowref, is_source):
130 conflict = tree_model.get_value(rowref, CONFLICT_IDX) 131 text = conflict.get_snippet(is_source) 132 cell_renderer.set_property("text", text)
133
134 - def _icon_data_func(self, column, cell_renderer, tree_model, rowref, is_source):
135 conflict = tree_model.get_value(rowref, CONFLICT_IDX) 136 icon = conflict.get_icon(is_source) 137 cell_renderer.set_property("pixbuf", icon)
138
139 - def _direction_data_func(self, column, cell_renderer, tree_model, rowref, user_data):
140 direction = tree_model.get_value(rowref, user_data) 141 if tree_model.iter_depth(rowref) == 0: 142 cell_renderer.set_property('visible', False) 143 cell_renderer.set_property('mode', gtk.CELL_RENDERER_MODE_INERT) 144 else: 145 cell_renderer.set_property('visible', True) 146 cell_renderer.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE) 147 cell_renderer.set_direction(direction)
148
149 - def _set_conflict_titles(self):
150 self.expander.set_label(_("Conflicts (%s)") % self.numConflicts) 151 self.standalone.set_title(_("Conflicts (%s)") % self.numConflicts)
152
153 - def on_conflict(self, cond, conflict):
154 #We start with the expander disabled. Make sure we only enable it once 155 if len(self.model) == 0: 156 self.expander.set_sensitive(True) 157 158 self.numConflicts += 1 159 source,sink = conflict.get_partnership() 160 if (source,sink) not in self.partnerships: 161 #create a header row 162 header = ConflictHeader(source, sink) 163 rowref = self.model.append(None, (header, Conflict.CONFLICT_ASK)) 164 self.partnerships[(source,sink)] = (rowref,conflict) 165 166 rowref = self.partnerships[(source,sink)][0] 167 self.model.append(rowref, (conflict, Conflict.CONFLICT_ASK))
168 169 #FIXME: Do this properly with model signals and a count function 170 #update the expander label and the standalone window title 171 #self._set_conflict_titles() 172
173 - def on_expand(self, sender):
174 #Force the vpane to move to the bottom 175 self.vpane.set_position(-1)
176
177 - def on_fullscreen_toggled(self, sender):
178 #switches between showing the conflicts in a standalone window. 179 #uses fullscreenButton.get_active() as a state variable 180 if self.fullscreenButton.get_active(): 181 self.expander.set_expanded(False) 182 self.fullscreenButton.set_image(gtk.image_new_from_icon_name("gtk-leave-fullscreen", gtk.ICON_SIZE_MENU)) 183 self.conflictScrolledWindow.reparent(self.standalone) 184 self.standalone.show() 185 self.expander.set_sensitive(False) 186 else: 187 self.fullscreenButton.set_image(gtk.image_new_from_icon_name("gtk-fullscreen", gtk.ICON_SIZE_MENU)) 188 self.conflictScrolledWindow.reparent(self.expander) 189 self.standalone.hide() 190 self.expander.set_sensitive(True)
191
192 - def on_standalone_closed(self, sender, event):
193 self.fullscreenButton.set_active(False) 194 self.on_fullscreen_toggled(sender) 195 return True
196
197 - def on_resolve_conflicts(self, sender):
198 #save the resolved rowrefs and remove them at the end 199 resolved = [] 200 201 def _resolve_func(model, path, rowref): 202 #skip header rows 203 if model.iter_depth(rowref) == 0: 204 return 205 206 direction = model[path][DIRECTION_IDX] 207 conflict = model[path][CONFLICT_IDX] 208 209 if conflict.resolve(direction): 210 resolved.append(rowref)
211 212 self.model.foreach(_resolve_func) 213 for r in resolved: 214 self.model.remove(r) 215 216 #now look for any sync partnerships with no children 217 empty = [] 218 for source,sink in self.partnerships: 219 rowref = self.partnerships[(source,sink)][0] 220 numChildren = self.model.iter_n_children(rowref) 221 if numChildren == 0: 222 sink.module.set_status(DataProvider.STATUS_DONE_SYNC_OK) 223 empty.append( (rowref, source, sink) ) 224 else: 225 sink.module.set_status(DataProvider.STATUS_DONE_SYNC_CONFLICT) 226 227 #do in two loops so as to not change the model while iterating 228 for rowref, source, sink in empty: 229 self.model.remove(rowref) 230 try: 231 del(self.partnerships[(source,sink)]) 232 except KeyError: pass
233
234 - def on_cancel_conflicts(self, sender):
235 self.model.clear() 236 self.partnerships = {} 237 self.numConflicts = 0 238 self._set_conflict_titles()
239
240 - def on_compare_conflicts(self, sender):
241 model, rowref = self.view.get_selection().get_selected() 242 conflict = model.get_value(rowref, CONFLICT_IDX) 243 Vfs.uri_open(conflict.sourceData.get_open_URI()) 244 Vfs.uri_open(conflict.sinkData.get_open_URI())
245
246 - def on_selection_changed(self, treeSelection):
247 """ 248 Makes the compare button active only if an open_URI for the data 249 has been set and its not a header row. 250 FIXME: In future could convert to text to allow user to compare that way 251 """ 252 model, rowref = treeSelection.get_selected() 253 #when the rowref under the selected row is removed by resolve thread 254 if rowref == None: 255 self.compareButton.set_sensitive(False) 256 else: 257 conflict = model.get_value(rowref, CONFLICT_IDX) 258 if model.iter_depth(rowref) == 0: 259 self.compareButton.set_sensitive(False) 260 #both must have an open_URI set to work 261 elif conflict.sourceData.get_open_URI() != None and conflict.sinkData.get_open_URI() != None: 262 self.compareButton.set_sensitive(True) 263 else: 264 self.compareButton.set_sensitive(False)
265
266 -class ConflictCellRenderer(gtk.GenericCellRenderer):
267 """ 268 An unfortunately neccessary wrapper around a CellRenderPixbuf because 269 said renderer is not activatable 270 """
271 - def __init__(self):
272 gtk.GenericCellRenderer.__init__(self) 273 self.image = None
274
275 - def on_get_size(self, widget, cell_area):
276 return ( 0,0, 277 16,16 278 )
279
280 - def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
281 if self.image != None: 282 middle_x = (cell_area.width - 16) / 2 283 middle_y = (cell_area.height - 16) / 2 284 self.image.render_to_drawable_alpha(window, 285 0, 0, #x, y in pixbuf 286 middle_x + cell_area.x, #middle x in drawable 287 middle_y + cell_area.y, #middle y in drawable 288 -1, -1, # use pixbuf width & height 289 0, 0, # alpha (deprecated params) 290 gtk.gdk.RGB_DITHER_NONE, 291 0, 0 292 ) 293 # self.image.draw_pixbuf( 294 # None, #gc for clipping 295 # window, #draw to 296 # 0, 0, #x, y in pixbuf 297 # cell_area.x, cell_area.y, # x, y in drawable 298 # -1, -1, # use pixbuf width & height 299 # gtk.gdk.RGB_DITHER_NONE, 300 # 0, 0 301 # ) 302 return True
303
304 - def set_direction(self, direction):
305 if direction == Conflict.CONFLICT_COPY_SINK_TO_SOURCE: 306 self.image = gtk.icon_theme_get_default().load_icon("conduit-conflict-left",16,0) 307 elif direction == Conflict.CONFLICT_COPY_SOURCE_TO_SINK: 308 self.image = gtk.icon_theme_get_default().load_icon("conduit-conflict-right",16,0) 309 elif direction == Conflict.CONFLICT_SKIP: 310 self.image = gtk.icon_theme_get_default().load_icon("conduit-conflict-skip",16,0) 311 elif direction == Conflict.CONFLICT_DELETE: 312 self.image = gtk.icon_theme_get_default().load_icon("conduit-conflict-delete",16,0) 313 elif direction == Conflict.CONFLICT_ASK: 314 self.image = gtk.icon_theme_get_default().load_icon("conduit-conflict-ask",16,0) 315 else: 316 self.image = None
317
318 - def on_activate(self, event, widget, path, background_area, cell_area, flags):
319 model = widget.get_model() 320 conflict = model[path][CONFLICT_IDX] 321 #Click toggles between --> and <-- and -x- but only within the list 322 #of valid choices. If at the end of the valid choices, then loop around 323 try: 324 curIdx = list(conflict.choices).index(model[path][DIRECTION_IDX]) 325 except ValueError: 326 #Because CONFLICT_ASK is never a valid choice, its just the default 327 #to make the user have to acknowledge the conflict 328 curIdx = 0 329 330 if curIdx == len(conflict.choices) - 1: 331 model[path][DIRECTION_IDX] = conflict.choices[0] 332 else: 333 model[path][DIRECTION_IDX] = conflict.choices[curIdx+1] 334 335 return True
336