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