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