Removed left-over obsolete statement.
[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         env.shelf.flushCategoryCache()
165         for category in self.__sortCategories(env.shelf.getRootCategories()):
166             self.__loadCategorySubTree(None, category)
167         if self.__objectCollection is not None:
168             self.objectSelectionChanged()
169
170     def setCollection(self, objectCollection):
171         if self.__objectCollection is not None:
172             self.__objectCollection.getObjectSelection().removeChangedCallback(self.objectSelectionChanged)
173         self.__objectCollection = objectCollection
174         self.__objectCollection.getObjectSelection().addChangedCallback(self.objectSelectionChanged)
175         self.objectSelectionChanged()
176
177     def objectSelectionChanged(self, objectSelection=None):
178         self.__updateToggleColumn()
179         self.__updateQSToggleColumn()
180         self.__updateContextMenu()
181         self.__expandAndCollapseRows(env.widgets["autoExpand"].get_active(),
182                                      env.widgets["autoCollapse"].get_active())
183
184
185 ###############################################################################
186 ### Callback functions registered by this class but invoked from other classes.
187
188     def _executeQuery(self, *foo):
189         query = self.__buildQueryFromSelection()
190         if query:
191             self.__mainWindow.loadQuery(query)
192
193     def _categoryViewFocusInEvent(self, widget, event):
194         self._menubarOids = []
195         for widgetName, function in [
196                 ("menubarCut", lambda *x: self._cutCategory(None, None)),
197                 ("menubarCopy", lambda *x: self._copyCategory(None, None)),
198                 ("menubarPaste", lambda *x: self._pasteCategory(None, None)),
199                 ("menubarDestroy", lambda *x: self._deleteCategories(None, None)),
200                 ("menubarClear", lambda *x: widget.get_selection().unselect_all()),
201                 ("menubarSelectAll", lambda *x: widget.get_selection().select_all()),
202                 ("menubarProperties", lambda *x: self._editProperties(None, None)),
203                 ]:
204             w = env.widgets[widgetName]
205             oid = w.connect("activate", function)
206             self._menubarOids.append((w, oid))
207         self.__updateContextMenu()
208
209     def _categoryViewFocusOutEvent(self, widget, event):
210         for (widget, oid) in self._menubarOids:
211             widget.disconnect(oid)
212
213     def _categorySelectionChanged(self, selection):
214         selectedCategoryRows = []
215         selection = self.__categoryView.get_selection()
216         # TODO replace with "get_selected_rows()" when it is introduced in Pygtk 2.2 API
217         selection.selected_foreach(lambda model,
218                                    path,
219                                    iter:
220                                    selectedCategoryRows.append(model[path]))
221         self.__selectedCategoriesIds  = {}
222
223         for categoryRow in selectedCategoryRows:
224             cid = categoryRow[self.__COLUMN_CATEGORY_ID]
225             # row.parent method gives assertion failed, dont know why. Using workaround instead.
226             parentPath = categoryRow.path[:-1]
227             if parentPath:
228                 parentId = categoryRow.model[parentPath][self.__COLUMN_CATEGORY_ID]
229             else:
230                 parentId = None
231             try:
232                  self.__selectedCategoriesIds[cid].append(parentId)
233             except KeyError:
234                  self.__selectedCategoriesIds[cid] = [parentId]
235         self.__updateContextMenu()
236         env.widgets["categorySearchButton"].set_sensitive(
237             len(selectedCategoryRows) > 0)
238
239     def _connectionToggled(self, renderer, path):
240         categoryRow = self.__categoryModel[path]
241         category = env.shelf.getCategory(categoryRow[self.__COLUMN_CATEGORY_ID])
242         if categoryRow[self.__COLUMN_INCONSISTENT] \
243                or not categoryRow[self.__COLUMN_CONNECTED]:
244             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
245                 try:
246                     obj.addCategory(category)
247                 except CategoryPresentError:
248                     # The object was already connected to the category
249                     pass
250             categoryRow[self.__COLUMN_INCONSISTENT] = False
251             categoryRow[self.__COLUMN_CONNECTED] = True
252         else:
253             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
254                 obj.removeCategory(category)
255             categoryRow[self.__COLUMN_CONNECTED] = False
256             categoryRow[self.__COLUMN_INCONSISTENT] = False
257         self.__updateToggleColumn()
258         self.__updateQSToggleColumn()
259
260     def _button_pressed(self, treeView, event):
261         if event.button == 3:
262             self._contextMenu.popup(None,None,None,event.button,event.time)
263             return True
264         rec = self.__categoryView.get_cell_area(0, self.__toggleColumn)
265         if event.x <= (rec.x + rec.width):
266             # Ignore selection event since the user clicked on the toggleColumn.
267             self.__ignoreSelectEvent = True
268         return False
269
270     def _button_released(self, treeView, event):
271         self.__ignoreSelectEvent = False
272         return False
273
274     def _rowActivated(self, a, b, c):
275         # TODO What should happen if the user dubble-click on a category?
276         pass
277
278     def _copyCategory(self, item, data):
279         cc = ClipboardCategories()
280         cc.type = cc.COPY
281         cc.categories = self.__selectedCategoriesIds
282         env.clipboard.setCategories(cc)
283
284     def _cutCategory(self, item, data):
285         cc = ClipboardCategories()
286         cc.type = cc.CUT
287         cc.categories = self.__selectedCategoriesIds
288         env.clipboard.setCategories(cc)
289
290     def _pasteCategory(self, item, data):
291         assert env.clipboard.hasCategories()
292         clipboardCategories = env.clipboard[0]
293         env.clipboard.clear()
294         try:
295             for (categoryId, previousParentIds) in clipboardCategories.categories.items():
296                 for newParentId in self.__selectedCategoriesIds:
297                     if clipboardCategories.type == ClipboardCategories.COPY:
298                         self.__connectChildToCategory(categoryId, newParentId)
299                         for parentId in previousParentIds:
300                             if parentId is None:
301                                 self.__disconnectChildHelper(categoryId, None,
302                                                              None, self.__categoryModel)
303                     else:
304                         if newParentId in previousParentIds:
305                             previousParentIds.remove(newParentId)
306                         else:
307                             self.__connectChildToCategory(categoryId, newParentId)
308                         for parentId in previousParentIds:
309                             if parentId is None:
310                                 self.__disconnectChildHelper(categoryId, None,
311                                                              None, self.__categoryModel)
312                             else:
313                                 self.__disconnectChild(categoryId, parentId)
314         except CategoryLoopError:
315             dialog = gtk.MessageDialog(
316                 type=gtk.MESSAGE_ERROR,
317                 buttons=gtk.BUTTONS_OK,
318                 message_format="Category loop detected.")
319             dialog.run()
320             dialog.destroy()
321         self.__updateToggleColumn()
322         self.__expandAndCollapseRows(False, False)
323
324     def _createRootCategory(self, item, data):
325         dialog = CategoryDialog("Create top-level category")
326         dialog.run(self._createRootCategoryHelper)
327
328     def _createRootCategoryHelper(self, tag, desc):
329         category = env.shelf.createCategory(tag, desc)
330         self.__loadCategorySubTree(None, category)
331
332     def _createChildCategory(self, item, data):
333         dialog = CategoryDialog("Create subcategory")
334         dialog.run(self._createChildCategoryHelper)
335
336     def _createChildCategoryHelper(self, tag, desc):
337         newCategory = env.shelf.createCategory(tag, desc)
338         for selectedCategoryId in self.__selectedCategoriesIds:
339             self.__connectChildToCategory(newCategory.getId(), selectedCategoryId)
340         self.__expandAndCollapseRows(False, False)
341
342     def _deleteCategories(self, item, data):
343         dialogId = "destroyCategoriesDialog"
344         widgets = gtk.glade.XML(env.gladeFile, dialogId)
345         dialog = widgets.get_widget(dialogId)
346         result = dialog.run()
347         if result == gtk.RESPONSE_OK:
348             for categoryId in self.__selectedCategoriesIds:
349                 category = env.shelf.getCategory(categoryId)
350                 for child in list(category.getChildren()):
351                     # The backend automatically disconnects childs
352                     # when a category is deleted, but we do it ourself
353                     # to make sure that the treeview widget is
354                     # updated.
355                     self.__disconnectChild(child.getId(), categoryId)
356                 env.shelf.deleteCategory(categoryId)
357                 env.shelf.flushCategoryCache()
358                 self.__forEachCategoryRow(
359                     self.__deleteCategoriesHelper, categoryId)
360         dialog.destroy()
361
362     def __deleteCategoriesHelper(self, categoryRow, categoryIdToDelete):
363         if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryIdToDelete:
364             self.__categoryModel.remove(categoryRow.iter)
365
366     def _disconnectCategory(self, item, data):
367         for (categoryId, parentIds) in self.__selectedCategoriesIds.items():
368             for parentId in parentIds:
369                 if not parentId == None: # Not possible to disconnect root categories
370                     self.__disconnectChild(categoryId, parentId)
371
372     def _editProperties(self, item, data):
373         for categoryId in self.__selectedCategoriesIds:
374             dialog = CategoryDialog("Change properties", categoryId)
375             dialog.run(self._editPropertiesHelper, data=categoryId)
376
377     def _editPropertiesHelper(self, tag, desc, categoryId):
378          category = env.shelf.getCategory(categoryId)
379          category.setTag(tag)
380          category.setDescription(desc)
381          env.shelf.flushCategoryCache()
382          self.__forEachCategoryRow(self.__updatePropertiesFromShelf, categoryId)
383
384     def _selectionFunction(self, path, b):
385         return not self.__ignoreSelectEvent
386
387     def _categoryQSViewFocusInEvent(self, widget, event):
388         self.__categoryQSEntry.grab_focus()
389
390     def _categoryQSEntryActivateEvent(self, entry):
391         if not self.__qsSelectedPath:
392             return
393         self._qsConnectionToggled(None, self.__qsSelectedPath)
394         self.__categoryQSFreeze = True
395         self.__categoryQSEntry.set_text("")
396         self.__categoryQSFreeze = False
397         self.__qsSelectedPath = None
398
399     def _categoryQSEntryChangedEvent(self, entry):
400         if self.__categoryQSFreeze:
401             return
402         self.__categoryQSModel.clear()
403         self.__qsSelectedPath = None
404         self.__categoryQSButton.set_sensitive(False)
405         text = entry.get_text().decode("utf-8")
406         if text == "":
407             return
408
409         regexp = re.compile(".*%s.*" % re.escape(text.lower()))
410         categories = list(env.shelf.getMatchingCategories(regexp))
411         categories.sort(self.__compareCategories)
412         exactMatches = []
413         for category in categories:
414             iterator = self.__categoryQSModel.append()
415             if (category.getTag().lower() == text.lower() or
416                 category.getDescription().lower() == text.lower()):
417                 exactMatches.append(self.__categoryQSModel.get_path(iterator))
418             self.__categoryQSModel.set_value(
419                 iterator, self.__COLUMN_CATEGORY_ID, category.getId())
420             self.__categoryQSModel.set_value(
421                 iterator,
422                 self.__COLUMN_DESCRIPTION,
423                 "%s [%s]" % (category.getDescription(), category.getTag()))
424         if len(categories) == 1:
425             self.__qsSelectedPath = (0,)
426             self.__categoryQSButton.set_sensitive(True)
427         elif len(exactMatches) == 1:
428             self.__qsSelectedPath = exactMatches[0]
429             self.__categoryQSButton.set_sensitive(True)
430         self.__updateQSToggleColumn()
431
432     def _qsConnectionToggled(self, renderer, path):
433         categoryRow = self.__categoryQSModel[path]
434         category = env.shelf.getCategory(
435             categoryRow[self.__COLUMN_CATEGORY_ID])
436         if categoryRow[self.__COLUMN_INCONSISTENT] \
437                or not categoryRow[self.__COLUMN_CONNECTED]:
438             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
439                 try:
440                     obj.addCategory(category)
441                 except CategoryPresentError:
442                     # The object was already connected to the category
443                     pass
444             categoryRow[self.__COLUMN_INCONSISTENT] = False
445             categoryRow[self.__COLUMN_CONNECTED] = True
446         else:
447             for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
448                 obj.removeCategory(category)
449             categoryRow[self.__COLUMN_CONNECTED] = False
450             categoryRow[self.__COLUMN_INCONSISTENT] = False
451         self.__updateToggleColumn()
452         self.__expandAndCollapseRows(
453             env.widgets["autoExpand"].get_active(),
454             env.widgets["autoCollapse"].get_active())
455
456 ######################################################################
457 ### Private
458
459     __cutCategoryLabel = "Cut"
460     __copyCategoryLabel = "Copy"
461     __pasteCategoryLabel = "Paste as child(ren)"
462     __destroyCategoryLabel = "Destroy..."
463     __disconnectCategoryLabel = "Disconnect from parent"
464     __createChildCategoryLabel = "Create subcategory..."
465     __createRootCategoryLabel = "Create top-level category..."
466     __propertiesLabel = "Properties"
467
468     __COLUMN_CATEGORY_ID  = 0
469     __COLUMN_DESCRIPTION  = 1
470     __COLUMN_CONNECTED    = 2
471     __COLUMN_INCONSISTENT = 3
472
473     def __loadCategorySubTree(self, parent, category):
474         # TODO Do we have to use iterators here or can we use pygtks simplified syntax?
475         iterator = self.__categoryModel.iter_children(parent)
476         while (iterator != None and
477                self.__categoryModel.get_value(iterator, self.__COLUMN_DESCRIPTION) <
478                    category.getDescription()):
479             iterator = self.__categoryModel.iter_next(iterator)
480         iterator = self.__categoryModel.insert_before(parent, iterator)
481         self.__categoryModel.set_value(iterator, self.__COLUMN_CATEGORY_ID, category.getId())
482         self.__categoryModel.set_value(iterator, self.__COLUMN_DESCRIPTION, category.getDescription())
483         self.__categoryModel.set_value(iterator, self.__COLUMN_CONNECTED, False)
484         self.__categoryModel.set_value(iterator, self.__COLUMN_INCONSISTENT, False)
485         for child in self.__sortCategories(category.getChildren()):
486             self.__loadCategorySubTree(iterator, child)
487
488     def __buildQueryFromSelection(self):
489         if env.widgets["categoriesOr"].get_active():
490             operator = " or "
491         else:
492             operator = " and "
493         return operator.join([env.shelf.getCategory(x).getTag()
494                               for x in self.__selectedCategoriesIds])
495
496     def __updateContextMenu(self):
497         # TODO Create helper functions to use from this method
498         menubarWidgetNames = [
499                 "menubarCut",
500                 "menubarCopy",
501                 "menubarPaste",
502                 "menubarDestroy",
503                 "menubarProperties",
504                 "menubarDisconnectFromParent",
505                 "menubarCreateChild",
506                 "menubarCreateRoot",
507                 ]
508         if len(self.__selectedCategoriesIds) == 0:
509             self._contextMenuGroup.disable()
510             for widgetName in menubarWidgetNames:
511                 env.widgets[widgetName].set_sensitive(False)
512             self._contextMenuGroup[
513                 self.__createRootCategoryLabel].set_sensitive(True)
514             env.widgets["menubarCreateRoot"].set_sensitive(True)
515         else:
516             self._contextMenuGroup.enable()
517             for widgetName in menubarWidgetNames:
518                 env.widgets[widgetName].set_sensitive(True)
519             if not env.clipboard.hasCategories():
520                 self._contextMenuGroup[
521                     self.__pasteCategoryLabel].set_sensitive(False)
522                 env.widgets["menubarPaste"].set_sensitive(False)
523         propertiesItem = self._contextMenuGroup[self.__propertiesLabel]
524         propertiesItemSensitive = len(self.__selectedCategoriesIds) == 1
525         propertiesItem.set_sensitive(propertiesItemSensitive)
526         env.widgets["menubarProperties"].set_sensitive(propertiesItemSensitive)
527
528     def __updateToggleColumn(self):
529         # find out which categories are connected, not connected or
530         # partitionally connected to selected objects
531         nrSelectedObjectsInCategory = {}
532         nrSelectedObjects = 0
533         for obj in self.__objectCollection.getObjectSelection().getSelectedObjects():
534             nrSelectedObjects += 1
535             for category in obj.getCategories():
536                 categoryId = category.getId()
537                 try:
538                     nrSelectedObjectsInCategory[categoryId] += 1
539                 except KeyError:
540                         nrSelectedObjectsInCategory[categoryId] = 1
541         self.__forEachCategoryRow(self.__updateToggleColumnHelper,
542                                   (nrSelectedObjects, nrSelectedObjectsInCategory))
543
544     def __updateQSToggleColumn(self):
545         selectedObjects = \
546             self.__objectCollection.getObjectSelection().getSelectedObjects()
547         nrSelectedObjectsInCategory = {}
548         nrSelectedObjects = 0
549         for obj in selectedObjects:
550             nrSelectedObjects += 1
551             for category in obj.getCategories():
552                 catid = category.getId()
553                 nrSelectedObjectsInCategory.setdefault(catid, 0)
554                 nrSelectedObjectsInCategory[catid] += 1
555         self.__forEachCategoryRow(
556             self.__updateToggleColumnHelper,
557             (nrSelectedObjects, nrSelectedObjectsInCategory),
558             self.__categoryQSModel)
559
560     def __updateToggleColumnHelper(self,
561                                    categoryRow,
562                                    (nrSelectedObjects, nrSelectedObjectsInCategory)):
563         categoryId = categoryRow[self.__COLUMN_CATEGORY_ID]
564         if categoryId in nrSelectedObjectsInCategory:
565             if nrSelectedObjectsInCategory[categoryId] < nrSelectedObjects:
566                 # Some of the selected objects are connected to the category
567                 categoryRow[self.__COLUMN_CONNECTED] = False
568                 categoryRow[self.__COLUMN_INCONSISTENT] = True
569             else:
570                 # All of the selected objects are connected to the category
571                 categoryRow[self.__COLUMN_CONNECTED] = True
572                 categoryRow[self.__COLUMN_INCONSISTENT] = False
573         else:
574             # None of the selected objects are connected to the category
575             categoryRow[self.__COLUMN_CONNECTED] = False
576             categoryRow[self.__COLUMN_INCONSISTENT] = False
577
578     def __forEachCategoryRow(self, function, data=None, categoryRows=None):
579         # We can't use gtk.TreeModel.foreach() since it does not pass a row
580         # to the callback function.
581         if not categoryRows:
582             categoryRows=self.__categoryModel
583         for categoryRow in categoryRows:
584             function(categoryRow, data)
585             self.__forEachCategoryRow(function, data, categoryRow.iterchildren())
586
587     def __expandAndCollapseRows(self, autoExpand, autoCollapse, categoryRows=None):
588         if categoryRows is None:
589             categoryRows=self.__categoryModel
590         someRowsExpanded = False
591         for categoryRow in categoryRows:
592             expandThisRow = False
593             # Expand all rows that are selected or has expanded childs
594             childRowsExpanded = self.__expandAndCollapseRows(autoExpand,
595                                                             autoCollapse,
596                                                             categoryRow.iterchildren())
597             if (childRowsExpanded
598                 or self.__categoryView.get_selection().path_is_selected(categoryRow.path)):
599                 expandThisRow = True
600             # Auto expand all rows that has a checked toggle
601             if autoExpand:
602                 if (categoryRow[self.__COLUMN_CONNECTED]
603                     or categoryRow[self.__COLUMN_INCONSISTENT]):
604                     expandThisRow = True
605             if expandThisRow:
606                 for a in range(len(categoryRow.path)):
607                     self.__categoryView.expand_row(categoryRow.path[:a+1], False)
608                 someRowsExpanded = True
609             # Auto collapse?
610             elif autoCollapse:
611                 self.__categoryView.collapse_row(categoryRow.path)
612         return someRowsExpanded
613
614     def __connectChildToCategory(self, childId, parentId):
615         try:
616             # Update shelf
617             childCategory = env.shelf.getCategory(childId)
618             parentCategory = env.shelf.getCategory(parentId)
619             parentCategory.connectChild(childCategory)
620             env.shelf.flushCategoryCache()
621             # Update widget modell
622             # If we reload the whole category tree from the shelf, we would lose
623             # the widgets information about current selected categories,
624             # expanded categories and the widget's scroll position. Hence,
625             # we update our previously loaded model instead.
626             self.__connectChildToCategoryHelper(parentId,
627                                                 childCategory,
628                                                 self.__categoryModel)
629         except CategoriesAlreadyConnectedError:
630             # This is okay.
631             pass
632
633     def __connectChildToCategoryHelper(self, parentId, childCategory, categoryRows):
634         for categoryRow in categoryRows:
635             if categoryRow[self.__COLUMN_CATEGORY_ID] == parentId:
636                 self.__loadCategorySubTree(categoryRow.iter, childCategory)
637             else:
638                 self.__connectChildToCategoryHelper(parentId, childCategory, categoryRow.iterchildren())
639
640     def __disconnectChild(self, childId, parentId):
641         # Update shelf
642         childCategory = env.shelf.getCategory(childId)
643         parentCategory = env.shelf.getCategory(parentId)
644         if childCategory in env.shelf.getRootCategories():
645             alreadyWasRootCategory = True
646         else:
647             alreadyWasRootCategory = False
648         parentCategory.disconnectChild(childCategory)
649         env.shelf.flushCategoryCache()
650         # Update widget modell.
651         # If we reload the whole category tree from the shelf, we would lose
652         # the widgets information about current selected categories,
653         # expanded categories and the widget's scroll position. Hence,
654         # we update our previously loaded model instead.
655         self.__disconnectChildHelper(childId,
656                                     parentId,
657                                     None,
658                                     self.__categoryModel)
659         if not alreadyWasRootCategory:
660             for c in env.shelf.getRootCategories():
661                 if c.getId() == childCategory.getId():
662                     self.__loadCategorySubTree(None, childCategory)
663                     break
664
665     def __disconnectChildHelper(self, wantedChildId, wantedParentId,
666                                 parentId, categoryRows):
667         for categoryRow in categoryRows:
668             cid = categoryRow[self.__COLUMN_CATEGORY_ID]
669             if cid == wantedChildId and parentId == wantedParentId:
670                 self.__categoryModel.remove(categoryRow.iter)
671             self.__disconnectChildHelper(wantedChildId, wantedParentId, cid, categoryRow.iterchildren())
672
673     def __updatePropertiesFromShelf(self, categoryRow, categoryId):
674         if categoryRow[self.__COLUMN_CATEGORY_ID] == categoryId:
675             category = env.shelf.getCategory(categoryId)
676             categoryRow[self.__COLUMN_DESCRIPTION] = category.getDescription()
677
678     def __sortCategories(self, categoryIter):
679         categories = list(categoryIter)
680         categories.sort(self.__compareCategories)
681         return categories
682
683     def __compareCategories(self, x, y):
684         return cmp(
685             (x.getDescription(), x.getTag()),
686             (y.getDescription(), y.getTag()))
687
688 class ClipboardCategories:
689     COPY = 1
690     CUT = 2
691     categories = None
692     type = None