7fea281873b1cfb3913cea79ad972651d5f29b15
[joel/kofoto.git] / src / gkofoto / gkofoto / categories.py
1 import gobject
2 import gtk
3 import re
4
5 from environment import env
6 from categorydialog import CategoryDialog
7 from menuhandler import *
8 from kofoto.search import *
9 from kofoto.shelf import *
10
11 class Categories:
12
13 ######################################################################
14 ### Public
15
16     def __init__(self, mainWindow):
17         self.__mainWindow = mainWindow
18         self.__toggleColumn = None
19         self.__objectCollection = None
20         self.__ignoreSelectEvent = False
21         self.__selectedCategoriesIds  = {}
22         self.__categoryModel = gtk.TreeStore(gobject.TYPE_INT,      # CATEGORY_ID
23                                              gobject.TYPE_STRING,   # DESCRIPTION
24                                              gobject.TYPE_BOOLEAN,  # CONNECTED
25                                              gobject.TYPE_BOOLEAN)  # INCONSISTENT
26
27         #
28         # Category tree view
29         #
30         self.__categoryView = env.widgets["categoryView"]
31         self.__categoryView.realize()
32         self.__categoryView.set_model(self.__categoryModel)
33         self.__categoryView.connect("focus-in-event", self._categoryViewFocusInEvent)
34         self.__categoryView.connect("focus-out-event", self._categoryViewFocusOutEvent)
35
36         # Create toggle column
37         toggleRenderer = gtk.CellRendererToggle()
38         toggleRenderer.connect("toggled", self._connectionToggled)
39         self.__toggleColumn = gtk.TreeViewColumn("",
40                                                  toggleRenderer,
41                                                  active=self.__COLUMN_CONNECTED,
42                                                  inconsistent=self.__COLUMN_INCONSISTENT)
43         self.__categoryView.append_column(self.__toggleColumn)
44
45         # Create text column
46         textRenderer = gtk.CellRendererText()
47         textColumn = gtk.TreeViewColumn("Category", textRenderer, text=self.__COLUMN_DESCRIPTION)
48         self.__categoryView.append_column(textColumn)
49         self.__categoryView.set_expander_column(textColumn)
50
51         #
52         # Category quick select view
53         # 
54         self.__categoryQSModel = gtk.ListStore(
55             gobject.TYPE_INT,      # CATEGORY_ID
56             gobject.TYPE_STRING,   # DESCRIPTION
57             gobject.TYPE_BOOLEAN,  # CONNECTED
58             gobject.TYPE_BOOLEAN)  # INCONSISTENT
59         self.__categoryQSView = env.widgets["categoryQuickSelectView"]
60         self.__categoryQSView.connect(
61             "focus-in-event", self._categoryQSViewFocusInEvent)
62         self.__categoryQSEntry = env.widgets["categoryQuickSelectEntry"]
63         self.__categoryQSEntry.connect(
64             "activate", self._categoryQSEntryActivateEvent)
65         self.__categoryQSEntry.connect(
66             "changed", self._categoryQSEntryChangedEvent)
67         self.__categoryQSButton = env.widgets["categoryQuickSelectButton"]
68         self.__categoryQSButton.connect(
69             "clicked", self._categoryQSEntryActivateEvent)
70         self.__categoryQSView.realize()
71         self.__categoryQSView.set_model(self.__categoryQSModel)
72         self.__categoryQSFreeze = False
73
74         # Create toggle column
75         toggleRenderer = gtk.CellRendererToggle()
76         toggleRenderer.connect("toggled", self._qsConnectionToggled)
77         self.__toggleQSColumn = gtk.TreeViewColumn(
78             "",
79             toggleRenderer,
80             active=self.__COLUMN_CONNECTED,
81             inconsistent=self.__COLUMN_INCONSISTENT)
82         self.__qsToggleColumn = gtk.TreeViewColumn(
83             "",
84             toggleRenderer,
85             active=self.__COLUMN_CONNECTED,
86             inconsistent=self.__COLUMN_INCONSISTENT)
87         self.__categoryQSView.append_column(self.__qsToggleColumn)
88
89         # Create text column
90         textRenderer = gtk.CellRendererText()
91         textColumn = gtk.TreeViewColumn(
92             "Category", textRenderer, text=self.__COLUMN_DESCRIPTION)
93         self.__categoryQSView.append_column(textColumn)
94         self.__categoryQSView.set_expander_column(textColumn)
95
96         # Create context menu
97         # TODO Is it possible to load a menu from a glade file instead?
98         #      If not, create some helper functions to construct the menu...
99         self._contextMenu = gtk.Menu()
100
101         self._contextMenuGroup = MenuGroup()
102         self._contextMenuGroup.addStockImageMenuItem(
103             self.__cutCategoryLabel,
104             gtk.STOCK_CUT,
105             self._cutCategory)
106         self._contextMenuGroup.addStockImageMenuItem(
107             self.__copyCategoryLabel,
108             gtk.STOCK_COPY,
109             self._copyCategory)
110         self._contextMenuGroup.addStockImageMenuItem(
111             self.__pasteCategoryLabel,
112             gtk.STOCK_PASTE,
113             self._pasteCategory)
114         self._contextMenuGroup.addStockImageMenuItem(
115             self.__destroyCategoryLabel,
116             gtk.STOCK_DELETE,
117             self._deleteCategories)
118         self._contextMenuGroup.addMenuItem(
119             self.__disconnectCategoryLabel,
120             self._disconnectCategory)
121         self._contextMenuGroup.addMenuItem(
122             self.__createChildCategoryLabel,
123             self._createChildCategory)
124         self._contextMenuGroup.addMenuItem(
125             self.__createRootCategoryLabel,
126             self._createRootCategory)
127         self._contextMenuGroup.addStockImageMenuItem(
128             self.__propertiesLabel,
129             gtk.STOCK_PROPERTIES,
130             self._editProperties)
131
132         for item in self._contextMenuGroup:
133             self._contextMenu.append(item)
134
135         env.widgets["categorySearchButton"].set_sensitive(False)
136
137         # Init menubar items.
138         env.widgets["menubarDisconnectFromParent"].connect(
139             "activate", self._disconnectCategory, None)
140         env.widgets["menubarCreateChild"].connect(
141             "activate", self._createChildCategory, None)
142         env.widgets["menubarCreateRoot"].connect(
143             "activate", self._createRootCategory, None)
144
145         # Init selection functions
146         categorySelection = self.__categoryView.get_selection()
147         categorySelection.set_mode(gtk.SELECTION_MULTIPLE)
148         categorySelection.set_select_function(self._selectionFunction, None)
149         categorySelection.connect("changed", self._categorySelectionChanged)
150         categoryQSSelection = self.__categoryQSView.get_selection()
151         categoryQSSelection.set_mode(gtk.SELECTION_NONE)
152
153         # Connect the rest of the UI events
154         self.__categoryView.connect("button_press_event", self._button_pressed)
155         self.__categoryView.connect("button_release_event", self._button_released)
156         self.__categoryView.connect("row-activated", self._rowActivated)
157         env.widgets["categorySearchButton"].connect('clicked', self._executeQuery)
158
159         self.loadCategoryTree()
160
161
162     def loadCategoryTree(self):
163         self.__categoryModel.clear()
164         self.__categoryList = []
165         env.shelf.flushCategoryCache()
166         for category in self.__sortCategories(env.shelf.getRootCategories()):
167             self.__loadCategorySubTree(None, category)
168         if self.__objectCollection is not None:
169             self.objectSelectionChanged()
170
171     def setCollection(self, objectCollection):
172         if self.__objectCollection is not None:
173             self.__objectCollection.getObjectSelection().removeChangedCallback(self.objectSelectionChanged)
174         self.__objectCollection = objectCollection
175         self.__objectCollection.getObjectSelection().addChangedCallback(self.objectSelectionChanged)
176         self.objectSelectionChanged()
177
178     def objectSelectionChanged(self, objectSelection=None):
179         self.__updateToggleColumn()
180         self.__updateQSToggleColumn()
181         self.__updateContextMenu()
182         self.__expandAndCollapseRows(env.widgets["autoExpand"].get_active(),
183                                      env.widgets["autoCollapse"].get_active())
184
185
186 ###############################################################################
187 ### Callback functions registered by this class but invoked from other classes.
188
189     def _executeQuery(self, *foo):
190         query = self.__buildQueryFromSelection()
191         if query:
192             self.__mainWindow.loadQuery(query)
193
194     def _categoryViewFocusInEvent(self, widget, event):
195         self._menubarOids = []
196         for widgetName, function in [
197                 ("menubarCut", lambda *x: self._cutCategory(None, None)),
198                 ("menubarCopy", lambda *x: self._copyCategory(None, None)),
199                 ("menubarPaste", lambda *x: self._pasteCategory(None, None)),
200                 ("menubarDestroy", lambda *x: self._deleteCategories(None, None)),
201                 ("menubarClear", lambda *x: widget.get_selection().unselect_all()),
202                 ("menubarSelectAll", lambda *x: widget.get_selection().select_all()),
203                 ("menubarProperties", lambda *x: self._editProperties(None, None)),
204                 ]:
205             w = env.widgets[widgetName]
206             oid = w.connect("activate", function)
207             self._menubarOids.append((w, oid))
208         self.__updateContextMenu()
209
210     def _categoryViewFocusOutEvent(self, widget, event):
211         for (widget, oid) in self._menubarOids:
212             widget.disconnect(oid)
213
214     def _categorySelectionChanged(self, selection):
215         selectedCategoryRows = []
216         selection = self.__categoryView.get_selection()
217         # TODO replace with "get_selected_rows()" when it is introduced in Pygtk 2.2 API
218         selection.selected_foreach(lambda model,
219                                    path,
220                                    iter:
221                                    selectedCategoryRows.append(model[path]))
222         self.__selectedCategoriesIds  = {}
223
224         for categoryRow in selectedCategoryRows:
225             cid = categoryRow[self.__COLUMN_CATEGORY_ID]
226             # row.parent method gives assertion failed, dont know why. Using workaround instead.
227             parentPath = categoryRow.path[:-1]
228             if parentPath:
229                 parentId = categoryRow.model[parentPath][self.__COLUMN_CATEGORY_ID]
230             else:
231                 parentId = None
232             try:
233                  self.__selectedCategoriesIds[cid].append(parentId)
234             except KeyError:
235                  self.__selectedCategoriesIds[cid] = [parentId]
236         self.__updateContextMenu()
237         env.widgets["categorySearchButton"].set_sensitive(
238             len(selectedCategoryRows) > 0)
239
240     def _connectionToggled(self, renderer, path):
241         categoryRow = self.__categoryModel[path]
242         category = env.shelf.getCategory(categoryRow[self.__COLUMN_CATEGORY_ID])
243         if categoryRow[self.__COLUMN_INCONSISTENT] \
244                or not categoryRow[self.__COLUMN_CONNECTED]:
245             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
246                 try:
247                     obj.addCategory(category)
248                 except CategoryPresentError:
249                     # The object was already connected to the category
250                     pass
251             categoryRow[self.__COLUMN_INCONSISTENT] = False
252             categoryRow[self.__COLUMN_CONNECTED] = True
253         else:
254             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
255                 obj.removeCategory(category)
256             categoryRow[self.__COLUMN_CONNECTED] = False
257             categoryRow[self.__COLUMN_INCONSISTENT] = False
258         self.__updateToggleColumn()
259         self.__updateQSToggleColumn()
260
261     def _button_pressed(self, treeView, event):
262         if event.button == 3:
263             self._contextMenu.popup(None,None,None,event.button,event.time)
264             return True
265         rec = self.__categoryView.get_cell_area(0, self.__toggleColumn)
266         if event.x <= (rec.x + rec.width):
267             # Ignore selection event since the user clicked on the toggleColumn.
268             self.__ignoreSelectEvent = True
269         return False
270
271     def _button_released(self, treeView, event):
272         self.__ignoreSelectEvent = False
273         return False
274
275     def _rowActivated(self, a, b, c):
276         # TODO What should happen if the user dubble-click on a category?
277         pass
278
279     def _copyCategory(self, item, data):
280         cc = ClipboardCategories()
281         cc.type = cc.COPY
282         cc.categories = self.__selectedCategoriesIds
283         env.clipboard.setCategories(cc)
284
285     def _cutCategory(self, item, data):
286         cc = ClipboardCategories()
287         cc.type = cc.CUT
288         cc.categories = self.__selectedCategoriesIds
289         env.clipboard.setCategories(cc)
290
291     def _pasteCategory(self, item, data):
292         assert env.clipboard.hasCategories()
293         clipboardCategories = env.clipboard[0]
294         env.clipboard.clear()
295         try:
296             for (categoryId, previousParentIds) in clipboardCategories.categories.items():
297                 for newParentId in self.__selectedCategoriesIds:
298                     if clipboardCategories.type == ClipboardCategories.COPY:
299                         self.__connectChildToCategory(categoryId, newParentId)
300                         for parentId in previousParentIds:
301                             if parentId is None:
302                                 self.__disconnectChildHelper(categoryId, None,
303                                                              None, self.__categoryModel)
304                     else:
305                         if newParentId in previousParentIds:
306                             previousParentIds.remove(newParentId)
307                         else:
308                             self.__connectChildToCategory(categoryId, newParentId)
309                         for parentId in previousParentIds:
310                             if parentId is None:
311                                 self.__disconnectChildHelper(categoryId, None,
312                                                              None, self.__categoryModel)
313                             else:
314                                 self.__disconnectChild(categoryId, parentId)
315         except CategoryLoopError:
316             dialog = gtk.MessageDialog(
317                 type=gtk.MESSAGE_ERROR,
318                 buttons=gtk.BUTTONS_OK,
319                 message_format="Category loop detected.")
320             dialog.run()
321             dialog.destroy()
322         self.__updateToggleColumn()
323         self.__expandAndCollapseRows(False, False)
324
325     def _createRootCategory(self, item, data):
326         dialog = CategoryDialog("Create top-level category")
327         dialog.run(self._createRootCategoryHelper)
328
329     def _createRootCategoryHelper(self, tag, desc):
330         category = env.shelf.createCategory(tag, desc)
331         self.__loadCategorySubTree(None, category)
332
333     def _createChildCategory(self, item, data):
334         dialog = CategoryDialog("Create subcategory")
335         dialog.run(self._createChildCategoryHelper)
336
337     def _createChildCategoryHelper(self, tag, desc):
338         newCategory = env.shelf.createCategory(tag, desc)
339         for selectedCategoryId in self.__selectedCategoriesIds:
340             self.__connectChildToCategory(newCategory.getId(), selectedCategoryId)
341         self.__expandAndCollapseRows(False, False)
342
343     def _deleteCategories(self, item, data):
344         dialogId = "destroyCategoriesDialog"
345         widgets = gtk.glade.XML(env.gladeFile, dialogId)
346         dialog = widgets.get_widget(dialogId)
347         result = dialog.run()
348         if result == gtk.RESPONSE_OK:
349             for categoryId in self.__selectedCategoriesIds:
350                 category = env.shelf.getCategory(categoryId)
351                 for child in list(category.getChildren()):
352                     # The backend automatically disconnects childs
353                     # when a category is deleted, but we do it ourself
354                     # to make sure that the treeview widget is
355                     # updated.
356                     self.__disconnectChild(child.getId(), categoryId)
357                 env.shelf.deleteCategory(categoryId)
358                 env.shelf.flushCategoryCache()
359                 self.__forEachCategoryRow(
360                     self.__deleteCategoriesHelper, categoryId)
361         dialog.destroy()
362
363     def __deleteCategoriesHelper(self, categoryRow, categoryIdToDelete):
364         if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryIdToDelete:
365             self.__categoryModel.remove(categoryRow.iter)
366
367     def _disconnectCategory(self, item, data):
368         for (categoryId, parentIds) in self.__selectedCategoriesIds.items():
369             for parentId in parentIds:
370                 if not parentId == None: # Not possible to disconnect root categories
371                     self.__disconnectChild(categoryId, parentId)
372
373     def _editProperties(self, item, data):
374         for categoryId in self.__selectedCategoriesIds:
375             dialog = CategoryDialog("Change properties", categoryId)
376             dialog.run(self._editPropertiesHelper, data=categoryId)
377
378     def _editPropertiesHelper(self, tag, desc, categoryId):
379          category = env.shelf.getCategory(categoryId)
380          category.setTag(tag)
381          category.setDescription(desc)
382          env.shelf.flushCategoryCache()
383          self.__forEachCategoryRow(self.__updatePropertiesFromShelf, categoryId)
384
385     def _selectionFunction(self, path, b):
386         return not self.__ignoreSelectEvent
387
388     def _categoryQSViewFocusInEvent(self, widget, event):
389         self.__categoryQSEntry.grab_focus()
390
391     def _categoryQSEntryActivateEvent(self, entry):
392         if not self.__qsSelectedPath:
393             return
394         self._qsConnectionToggled(None, self.__qsSelectedPath)
395         self.__categoryQSFreeze = True
396         self.__categoryQSEntry.set_text("")
397         self.__categoryQSFreeze = False
398         self.__qsSelectedPath = None
399
400     def _categoryQSEntryChangedEvent(self, entry):
401         if self.__categoryQSFreeze:
402             return
403         self.__categoryQSModel.clear()
404         self.__qsSelectedPath = None
405         self.__categoryQSButton.set_sensitive(False)
406         text = entry.get_text().decode("utf-8")
407         if text == "":
408             return
409
410         regexp = re.compile(".*%s.*" % re.escape(text.lower()))
411         categories = list(env.shelf.getMatchingCategories(regexp))
412         categories.sort(self.__compareCategories)
413         exactMatches = []
414         for category in categories:
415             iterator = self.__categoryQSModel.append()
416             if (category.getTag().lower() == text.lower() or
417                 category.getDescription().lower() == text.lower()):
418                 exactMatches.append(self.__categoryQSModel.get_path(iterator))
419             self.__categoryQSModel.set_value(
420                 iterator, self.__COLUMN_CATEGORY_ID, category.getId())
421             self.__categoryQSModel.set_value(
422                 iterator,
423                 self.__COLUMN_DESCRIPTION,
424                 "%s [%s]" % (category.getDescription(), category.getTag()))
425         if len(categories) == 1:
426             self.__qsSelectedPath = (0,)
427             self.__categoryQSButton.set_sensitive(True)
428         elif len(exactMatches) == 1:
429             self.__qsSelectedPath = exactMatches[0]
430             self.__categoryQSButton.set_sensitive(True)
431         self.__updateQSToggleColumn()
432
433     def _qsConnectionToggled(self, renderer, path):
434         categoryRow = self.__categoryQSModel[path]
435         category = env.shelf.getCategory(
436             categoryRow[self.__COLUMN_CATEGORY_ID])
437         if categoryRow[self.__COLUMN_INCONSISTENT] \
438                or not categoryRow[self.__COLUMN_CONNECTED]:
439             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
440                 try:
441                     obj.addCategory(category)
442                 except CategoryPresentError:
443                     # The object was already connected to the category
444                     pass
445             categoryRow[self.__COLUMN_INCONSISTENT] = False
446             categoryRow[self.__COLUMN_CONNECTED] = True
447         else:
448             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
449                 obj.removeCategory(category)
450             categoryRow[self.__COLUMN_CONNECTED] = False
451             categoryRow[self.__COLUMN_INCONSISTENT] = False
452         self.__updateToggleColumn()
453         self.__expandAndCollapseRows(
454             env.widgets["autoExpand"].get_active(),
455             env.widgets["autoCollapse"].get_active())
456
457 ######################################################################
458 ### Private
459
460     __cutCategoryLabel = "Cut"
461     __copyCategoryLabel = "Copy"
462     __pasteCategoryLabel = "Paste as child(ren)"
463     __destroyCategoryLabel = "Destroy..."
464     __disconnectCategoryLabel = "Disconnect from parent"
465     __createChildCategoryLabel = "Create subcategory..."
466     __createRootCategoryLabel = "Create top-level category..."
467     __propertiesLabel = "Properties"
468
469     __COLUMN_CATEGORY_ID  = 0
470     __COLUMN_DESCRIPTION  = 1
471     __COLUMN_CONNECTED    = 2
472     __COLUMN_INCONSISTENT = 3
473
474     def __loadCategorySubTree(self, parent, category):
475         # TODO Do we have to use iterators here or can we use pygtks simplified syntax?
476         iterator = self.__categoryModel.iter_children(parent)
477         while (iterator != None and
478                self.__categoryModel.get_value(iterator, self.__COLUMN_DESCRIPTION) <
479                    category.getDescription()):
480             iterator = self.__categoryModel.iter_next(iterator)
481         iterator = self.__categoryModel.insert_before(parent, iterator)
482         self.__categoryModel.set_value(iterator, self.__COLUMN_CATEGORY_ID, category.getId())
483         self.__categoryModel.set_value(iterator, self.__COLUMN_DESCRIPTION, category.getDescription())
484         self.__categoryModel.set_value(iterator, self.__COLUMN_CONNECTED, False)
485         self.__categoryModel.set_value(iterator, self.__COLUMN_INCONSISTENT, False)
486         for child in self.__sortCategories(category.getChildren()):
487             self.__loadCategorySubTree(iterator, child)
488
489     def __buildQueryFromSelection(self):
490         if env.widgets["categoriesOr"].get_active():
491             operator = " or "
492         else:
493             operator = " and "
494         return operator.join([env.shelf.getCategory(x).getTag()
495                               for x in self.__selectedCategoriesIds])
496
497     def __updateContextMenu(self):
498         # TODO Create helper functions to use from this method
499         menubarWidgetNames = [
500                 "menubarCut",
501                 "menubarCopy",
502                 "menubarPaste",
503                 "menubarDestroy",
504                 "menubarProperties",
505                 "menubarDisconnectFromParent",
506                 "menubarCreateChild",
507                 "menubarCreateRoot",
508                 ]
509         if len(self.__selectedCategoriesIds) == 0:
510             self._contextMenuGroup.disable()
511             for widgetName in menubarWidgetNames:
512                 env.widgets[widgetName].set_sensitive(False)
513             self._contextMenuGroup[
514                 self.__createRootCategoryLabel].set_sensitive(True)
515             env.widgets["menubarCreateRoot"].set_sensitive(True)
516         else:
517             self._contextMenuGroup.enable()
518             for widgetName in menubarWidgetNames:
519                 env.widgets[widgetName].set_sensitive(True)
520             if not env.clipboard.hasCategories():
521                 self._contextMenuGroup[
522                     self.__pasteCategoryLabel].set_sensitive(False)
523                 env.widgets["menubarPaste"].set_sensitive(False)
524         propertiesItem = self._contextMenuGroup[self.__propertiesLabel]
525         propertiesItemSensitive = len(self.__selectedCategoriesIds) == 1
526         propertiesItem.set_sensitive(propertiesItemSensitive)
527         env.widgets["menubarProperties"].set_sensitive(propertiesItemSensitive)
528
529     def __updateToggleColumn(self):
530         # find out which categories are connected, not connected or
531         # partitionally connected to selected objects
532         nrSelectedObjectsInCategory = {}
533         nrSelectedObjects = 0
534         for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
535             nrSelectedObjects += 1
536             for category in obj.getCategories():
537                 categoryId = category.getId()
538                 try:
539                     nrSelectedObjectsInCategory[categoryId] += 1
540                 except KeyError:
541                         nrSelectedObjectsInCategory[categoryId] = 1
542         self.__forEachCategoryRow(self.__updateToggleColumnHelper,
543                                   (nrSelectedObjects, nrSelectedObjectsInCategory))
544
545     def __updateQSToggleColumn(self):
546         selectedObjects = \
547             self.__objectCollection.getObjectSelection().getSelectedObjects()
548         nrSelectedObjectsInCategory = {}
549         nrSelectedObjects = 0
550         for obj in selectedObjects:
551             nrSelectedObjects += 1
552             for category in obj.getCategories():
553                 catid = category.getId()
554                 nrSelectedObjectsInCategory.setdefault(catid, 0)
555                 nrSelectedObjectsInCategory[catid] += 1
556         self.__forEachCategoryRow(
557             self.__updateToggleColumnHelper,
558             (nrSelectedObjects, nrSelectedObjectsInCategory),
559             self.__categoryQSModel)
560
561     def __updateToggleColumnHelper(self,
562                                    categoryRow,
563                                    (nrSelectedObjects, nrSelectedObjectsInCategory)):
564         categoryId = categoryRow[self.__COLUMN_CATEGORY_ID]
565         if categoryId in nrSelectedObjectsInCategory:
566             if nrSelectedObjectsInCategory[categoryId] < nrSelectedObjects:
567                 # Some of the selected objects are connected to the category
568                 categoryRow[self.__COLUMN_CONNECTED] = False
569                 categoryRow[self.__COLUMN_INCONSISTENT] = True
570             else:
571                 # All of the selected objects are connected to the category
572                 categoryRow[self.__COLUMN_CONNECTED] = True
573                 categoryRow[self.__COLUMN_INCONSISTENT] = False
574         else:
575             # None of the selected objects are connected to the category
576             categoryRow[self.__COLUMN_CONNECTED] = False
577             categoryRow[self.__COLUMN_INCONSISTENT] = False
578
579     def __forEachCategoryRow(self, function, data=None, categoryRows=None):
580         # We can't use gtk.TreeModel.foreach() since it does not pass a row
581         # to the callback function.
582         if not categoryRows:
583             categoryRows=self.__categoryModel
584         for categoryRow in categoryRows:
585             function(categoryRow, data)
586             self.__forEachCategoryRow(function, data, categoryRow.iterchildren())
587
588     def __expandAndCollapseRows(self, autoExpand, autoCollapse, categoryRows=None):
589         if categoryRows is None:
590             categoryRows=self.__categoryModel
591         someRowsExpanded = False
592         for categoryRow in categoryRows:
593             expandThisRow = False
594             # Expand all rows that are selected or has expanded childs
595             childRowsExpanded = self.__expandAndCollapseRows(autoExpand,
596                                                             autoCollapse,
597                                                             categoryRow.iterchildren())
598             if (childRowsExpanded
599                 or self.__categoryView.get_selection().path_is_selected(categoryRow.path)):
600                 expandThisRow = True
601             # Auto expand all rows that has a checked toggle
602             if autoExpand:
603                 if (categoryRow[self.__COLUMN_CONNECTED]
604                     or categoryRow[self.__COLUMN_INCONSISTENT]):
605                     expandThisRow = True
606             if expandThisRow:
607                 for a in range(len(categoryRow.path)):
608                     self.__categoryView.expand_row(categoryRow.path[:a+1], False)
609                 someRowsExpanded = True
610             # Auto collapse?
611             elif autoCollapse:
612                 self.__categoryView.collapse_row(categoryRow.path)
613         return someRowsExpanded
614
615     def __connectChildToCategory(self, childId, parentId):
616         try:
617             # Update shelf
618             childCategory = env.shelf.getCategory(childId)
619             parentCategory = env.shelf.getCategory(parentId)
620             parentCategory.connectChild(childCategory)
621             env.shelf.flushCategoryCache()
622             # Update widget modell
623             # If we reload the whole category tree from the shelf, we would lose
624             # the widgets information about current selected categories,
625             # expanded categories and the widget's scroll position. Hence,
626             # we update our previously loaded model instead.
627             self.__connectChildToCategoryHelper(parentId,
628                                                 childCategory,
629                                                 self.__categoryModel)
630         except CategoriesAlreadyConnectedError:
631             # This is okay.
632             pass
633
634     def __connectChildToCategoryHelper(self, parentId, childCategory, categoryRows):
635         for categoryRow in categoryRows:
636             if categoryRow[self.__COLUMN_CATEGORY_ID] == parentId:
637                 self.__loadCategorySubTree(categoryRow.iter, childCategory)
638             else:
639                 self.__connectChildToCategoryHelper(parentId, childCategory, categoryRow.iterchildren())
640
641     def __disconnectChild(self, childId, parentId):
642         # Update shelf
643         childCategory = env.shelf.getCategory(childId)
644         parentCategory = env.shelf.getCategory(parentId)
645         if childCategory in env.shelf.getRootCategories():
646             alreadyWasRootCategory = True
647         else:
648             alreadyWasRootCategory = False
649         parentCategory.disconnectChild(childCategory)
650         env.shelf.flushCategoryCache()
651         # Update widget modell.
652         # If we reload the whole category tree from the shelf, we would lose
653         # the widgets information about current selected categories,
654         # expanded categories and the widget's scroll position. Hence,
655         # we update our previously loaded model instead.
656         self.__disconnectChildHelper(childId,
657                                     parentId,
658                                     None,
659                                     self.__categoryModel)
660         if not alreadyWasRootCategory:
661             for c in env.shelf.getRootCategories():
662                 if c.getId() == childCategory.getId():
663                     self.__loadCategorySubTree(None, childCategory)
664                     break
665
666     def __disconnectChildHelper(self, wantedChildId, wantedParentId,
667                                 parentId, categoryRows):
668         for categoryRow in categoryRows:
669             cid = categoryRow[self.__COLUMN_CATEGORY_ID]
670             if cid == wantedChildId and parentId == wantedParentId:
671                 self.__categoryModel.remove(categoryRow.iter)
672             self.__disconnectChildHelper(wantedChildId, wantedParentId, cid, categoryRow.iterchildren())
673
674     def __updatePropertiesFromShelf(self, categoryRow, categoryId):
675         if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryId:
676             category = env.shelf.getCategory(categoryId)
677             categoryRow[self.__COLUMN_DESCRIPTION] = category.getDescription()
678
679     def __sortCategories(self, categoryIter):
680         categories = list(categoryIter)
681         categories.sort(self.__compareCategories)
682         return categories
683
684     def __compareCategories(self, x, y):
685         return cmp(
686             (x.getDescription(), x.getTag()),
687             (y.getDescription(), y.getTag()))
688
689 class ClipboardCategories:
690     COPY = 1
691     CUT = 2
692     categories = None
693     type = None