Use list.sort's keyword parameters
[joel/kofoto.git] / src / packages / kofoto / gkofoto / tableview.py
1 import gtk
2 import gobject
3 import operator
4 from kofoto.gkofoto.environment import env
5 from kofoto.gkofoto.objectcollectionview import ObjectCollectionView
6 from kofoto.gkofoto.objectcollection import ObjectCollection
7 from kofoto.gkofoto.menuhandler import MenuGroup
8
9 class TableView(ObjectCollectionView):
10
11 ###############################################################################
12 ### Public
13
14     def __init__(self):
15         env.debug("Init TableView")
16         ObjectCollectionView.__init__(self, env.widgets["tableView"])
17         selection = self._viewWidget.get_selection()
18         selection.set_mode(gtk.SELECTION_MULTIPLE)
19         self.__viewGroup = None
20         self.__selectionLocked = False
21         self._viewWidget.connect("drag_data_received", self._onDragDataReceived)
22         self._viewWidget.connect("drag-data-get", self._onDragDataGet)
23         self._viewWidget.connect("row-activated", self._onRowActivated_cb)
24         self.__userChosenColumns = {}
25         self.__createdColumns = {}
26         self.__editedCallbacks = {}
27         self._connectedOids = []
28         self.__hasFocus = False
29         # Import the users setting in the configuration file for
30         # which columns that shall be shown.
31         columnLocation = 0
32         for columnName in env.defaultTableViewColumns:
33             self.__userChosenColumns[columnName] = columnLocation
34             columnLocation += 1
35         env.widgets["tableView"].connect("button_press_event", self._mouse_button_pressed)
36         env.widgets["menubarViewDetailsPane"].set_sensitive(False)
37
38     def importSelection(self, objectSelection):
39         if not self.__selectionLocked:
40             env.debug("TableView is importing selection")
41             self.__selectionLocked = True
42             selection = self._viewWidget.get_selection()
43             selection.unselect_all()
44             for rowNr in objectSelection:
45                 selection.select_path(rowNr)
46             rowNr = self._objectCollection.getObjectSelection().getLowestSelectedRowNr()
47             if rowNr is not None:
48                 # Scroll to first selected object in view
49                 self._viewWidget.scroll_to_cell(rowNr, None, False, 0, 0)
50             self.__selectionLocked = False
51         self._updateContextMenu()
52         self._updateMenubarSortMenu()
53
54     def fieldsDisabled(self, fields):
55         env.debug("Table view disable fields: " + str(fields))
56         self.__removeColumnsAndUpdateLocation(fields)
57         for columnName in fields:
58             self.__viewGroup[columnName].set_sensitive(False)
59
60     def fieldsEnabled(self, fields):
61         env.debug("Table view enable fields: " + str(fields))
62         objectMetadataMap = self._objectCollection.getObjectMetadataMap()
63         for columnName in fields:
64             self.__viewGroup[columnName].set_sensitive(True)
65             if columnName not in self.__createdColumns:
66                 if columnName in self.__userChosenColumns:
67                     self.__createColumn(columnName, objectMetadataMap, self.__userChosenColumns[columnName])
68
69     def _showHelper(self):
70         env.enter("TableView.showHelper()")
71         env.widgets["tableViewScroll"].show()
72         self._viewWidget.grab_focus()
73         env.exit("TableView.showHelper()")
74
75     def _hideHelper(self):
76         env.enter("TableView.hideHelper()")
77         env.widgets["tableViewScroll"].hide()
78         env.exit("TableView.hideHelper()")
79
80     def _connectObjectCollectionHelper(self):
81         env.enter("Connecting TableView to object collection")
82         # Set model
83         self._viewWidget.set_model(self._objectCollection.getModel())
84         # Create columns
85         objectMetadataMap = self._objectCollection.getObjectMetadataMap()
86         disabledFields = self._objectCollection.getDisabledFields()
87         columnLocationList = self.__userChosenColumns.items()
88         columnLocationList.sort(key=operator.itemgetter(1))
89         env.debug("Column locations: " + str(columnLocationList))
90         for (columnName, _) in columnLocationList:
91             if (columnName in objectMetadataMap and
92                 columnName not in disabledFields):
93                 self.__createColumn(columnName, objectMetadataMap)
94                 self.__viewGroup[columnName].activate()
95         self.fieldsDisabled(self._objectCollection.getDisabledFields())
96         env.exit("Connecting TableView to object collection")
97
98     def _initDragAndDrop(self):
99         # Init drag & drop
100         if self._objectCollection.isReorderable() and not self._objectCollection.isSortable():
101             targetEntries = [("STRING", gtk.TARGET_SAME_WIDGET, 0)]
102             self._viewWidget.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
103                                                       targetEntries,
104                                                       gtk.gdk.ACTION_MOVE)
105             self._viewWidget.enable_model_drag_dest(targetEntries, gtk.gdk.ACTION_COPY)
106         else:
107             self._viewWidget.unset_rows_drag_source()
108             self._viewWidget.unset_rows_drag_dest()
109
110     def _disconnectObjectCollectionHelper(self):
111         env.enter("Disconnecting TableView from object collection")
112         self.__removeColumnsAndUpdateLocation()
113         self._viewWidget.set_model(None)
114         env.exit("Disconnecting TableView from object collection")
115
116     def _freezeHelper(self):
117         env.enter("TableView.freezeHelper()")
118         self._clearAllConnections()
119         env.exit("TableView.freezeHelper()")
120
121     def _thawHelper(self):
122         env.enter("TableView.thawHelper()")
123         self._initDragAndDrop()
124         self._connect(
125             self._viewWidget, "focus-in-event", self._treeViewFocusInEvent)
126         self._connect(
127             self._viewWidget, "focus-out-event", self._treeViewFocusOutEvent)
128         self._connect(
129             self._viewWidget.get_selection(), "changed", self._widgetSelectionChanged)
130         env.exit("TableView.thawHelper()")
131
132     def _createContextMenu(self, objectCollection):
133         ObjectCollectionView._createContextMenu(self, objectCollection)
134         self.__viewGroup = self.__createTableColumnsMenuGroup(objectCollection)
135         self._contextMenu.add(self.__viewGroup.createGroupMenuItem())
136
137     def __createTableColumnsMenuGroup(self, objectCollection):
138         menuGroup = MenuGroup("View columns")
139         columnNames = objectCollection.getObjectMetadataMap().keys()
140         columnNames.sort()
141         for columnName in columnNames:
142             menuGroup.addCheckedMenuItem(
143                 columnName,
144                 self._viewColumnToggled,
145                 columnName)
146         return menuGroup
147
148     def _clearContextMenu(self):
149         ObjectCollectionView._clearContextMenu(self)
150         self.__viewGroup = None
151
152     def _hasFocus(self):
153         return self.__hasFocus
154
155 ###############################################################################
156 ### Callback functions registered by this class but invoked from other classes.
157
158     def _treeViewFocusInEvent(self, widget, unused1, unused2):
159         if self.__hasFocus:
160             # Work-around for some bug that makes the focus-out signal
161             # disappear.
162             return
163         self.__hasFocus = True
164         oc = self._objectCollection
165         for widgetName, function in [
166                 ("menubarCut", self._objectCollection.cut),
167                 ("menubarCopy", self._objectCollection.copy),
168                 ("menubarPaste", self._objectCollection.paste),
169                 ("menubarDelete", self._objectCollection.delete),
170                 ("menubarDestroy", oc.destroy),
171                 ("menubarClear", lambda x: widget.get_selection().unselect_all()),
172                 ("menubarSelectAll", lambda x: widget.get_selection().select_all()),
173                 ("menubarProperties", oc.albumProperties),
174                 ("menubarCreateAlbumChild", oc.createAlbumChild),
175                 ("menubarRegisterAndAddImages", oc.registerAndAddImages),
176                 ("menubarGenerateHtml", oc.generateHtml),
177                 ("menubarOpenImage", oc.openImage),
178                 ("menubarDuplicateAndOpenImage", oc.duplicateAndOpenImage),
179                 ("menubarRotateLeft", oc.rotateImageLeft),
180                 ("menubarRotateRight", oc.rotateImageRight),
181                 ("menubarImageVersions", oc.imageVersions),
182                 ("menubarRegisterImageVersions", oc.registerImageVersions),
183                 ("menubarMergeImages", oc.mergeImages),
184                 ]:
185             w = env.widgets[widgetName]
186             oid = w.connect("activate", function)
187             self._connectedOids.append((w, oid))
188
189         self._updateContextMenu()
190
191         for widgetName in [
192                 "menubarClear",
193                 "menubarSelectAll"
194                 ]:
195             env.widgets[widgetName].set_sensitive(True)
196
197     def _treeViewFocusOutEvent(self, widget, unused1, unused2):
198         self.__hasFocus = False
199         for (widget, oid) in self._connectedOids:
200             widget.disconnect(oid)
201         self._connectedOids = []
202         for widgetName in [
203                 "menubarCut",
204                 "menubarCopy",
205                 "menubarPaste",
206                 "menubarDelete",
207                 "menubarDestroy",
208                 "menubarClear",
209                 "menubarSelectAll",
210                 "menubarProperties",
211                 "menubarCreateAlbumChild",
212                 "menubarRegisterAndAddImages",
213                 "menubarGenerateHtml",
214                 "menubarOpenImage",
215                 "menubarDuplicateAndOpenImage",
216                 "menubarRotateLeft",
217                 "menubarRotateRight",
218                 "menubarImageVersions",
219                 "menubarRegisterImageVersions",
220                 "menubarMergeImages",
221                 ]:
222             env.widgets[widgetName].set_sensitive(False)
223
224     def _widgetSelectionChanged(self, selection, unused):
225         if not self.__selectionLocked:
226             env.enter("TableView selection changed")
227             self.__selectionLocked = True
228             rowNrs = []
229             selection.selected_foreach(lambda model,
230                                        path,
231                                        iter:
232                                        rowNrs.append(path[0]))
233             self._objectCollection.getObjectSelection().setSelection(rowNrs)
234             self.__selectionLocked = False
235             env.exit("TableView selection changed")
236
237     def _onDragDataGet(self, unused1, unused2, selection, unused3, unused4):
238         selectedRows = []
239         # TODO replace with "get_selected_rows()" when it is introduced in Pygtk 2.2 API
240         self._viewWidget.get_selection().selected_foreach(lambda model,
241                                                           path,
242                                                           iter:
243                                                           selectedRows.append(model[path]))
244         if len(selectedRows) == 1:
245             # Ignore drag & drop if zero or more then one row is selected
246             # Drag & drop of multiple rows will probably come in gtk 2.4.
247             # http://mail.gnome.org/archives/gtk-devel-list/2003-December/msg00160.html
248             sourceRowNumber = str(selectedRows[0].path[0])
249             selection.set_text(sourceRowNumber, len(sourceRowNumber))
250         else:
251             env.debug("Ignoring drag&drop when only one row is selected")
252
253
254     def _onDragDataReceived(self, treeview, dragContext, x, y, selection, unused, eventtime):
255         targetData = treeview.get_dest_row_at_pos(x, y)
256         if selection.get_text() == None:
257             dragContext.finish(False, False, eventtime)
258         else:
259             model = self._objectCollection.getModel()
260             if targetData == None:
261                 targetPath = (len(model) - 1,)
262                 dropPosition = gtk.TREE_VIEW_DROP_AFTER
263             else:
264                 targetPath, dropPosition = targetData
265             sourceRowNumber = int(selection.get_text())
266             if sourceRowNumber == targetPath[0]:
267                 # dropped on itself
268                 dragContext.finish(False, False, eventtime)
269             else:
270                 # The continer must have a getChildren() and a setChildren()
271                 # method as for example the album class has.
272                 container = self._objectCollection.getContainer()
273                 children = list(container.getChildren())
274                 sourceRow = model[sourceRowNumber]
275                 targetIter = model.get_iter(targetPath)
276                 objectSelection = self._objectCollection.getObjectSelection()
277                 if (dropPosition == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE
278                     or dropPosition == gtk.TREE_VIEW_DROP_BEFORE):
279                     container.setChildren(self.__moveListItem(children,
280                                                               sourceRowNumber,
281                                                               targetPath[0]))
282                     model.insert_before(sibling=targetIter, row=sourceRow)
283                     self._objectCollection.signalRowInserted()
284                     model.remove(sourceRow.iter)
285                     # TODO update the album tree widget?
286                 elif (dropPosition == gtk.TREE_VIEW_DROP_INTO_OR_AFTER
287                       or dropPosition == gtk.TREE_VIEW_DROP_AFTER):
288                     container.setChildren(self.__moveListItem(children,
289                                                               sourceRowNumber,
290                                                               targetPath[0] + 1))
291                     model.insert_after(sibling=targetIter, row=sourceRow)
292                     model.remove(sourceRow.iter)
293                     # TODO update the album tree widget?
294                 objectSelection.setSelection([targetPath[0]])
295                 # I've experienced that the drag-data-delete signal isn't
296                 # always emitted when I drag & drop rapidly in the TreeView.
297                 # And when it is missing the source row is not removed as is
298                 # should. It is probably an bug in gtk+ (or maybe in pygtk).
299                 # It only happens sometimes and I have not managed to reproduce
300                 # it with a simpler example. Hence we remove the row ourself
301                 # and are not relying on the drag-data-delete-signal.
302                 # http://bugzilla.gnome.org/show_bug.cgi?id=134997
303                 removeSourceRowAutomatically = False
304                 dragContext.finish(True, removeSourceRowAutomatically, eventtime)
305
306     def _onRowActivated_cb(self, widget, path, view_column):
307         model = self._objectCollection.getModel()
308         row = model[path]
309         if not row[ObjectCollection.COLUMN_IS_ALBUM]:
310             env.widgets["objectViewToggleButton"].set_active(True)
311
312     def _viewColumnToggled(self, checkMenuItem, columnName):
313         if checkMenuItem.get_active():
314             if columnName not in self.__createdColumns:
315                 self.__createColumn(columnName,
316                                     self._objectCollection.getObjectMetadataMap())
317                 # The correct columnLocation is stored when the column is removed
318                 # there is no need to store the location when it is created
319                 # since the column order may be reordered later before it is removed.
320         else:
321             # Since the column has been removed explicitly by the user
322             # we dont store the column's relative location.
323             try:
324                 del self.__userChosenColumns[columnName]
325             except KeyError:
326                 pass
327             if columnName in self.__createdColumns:
328                 self.__removeColumn(columnName)
329
330     def _reloadSingleObjectView(self):
331         pass
332
333 ###############################################################################
334 ### Private
335
336     def __createColumn(self, columnName, objectMetadataMap, location=-1):
337         (objtype, column, editedCallback, editedCallbackData) = objectMetadataMap[columnName]
338         if objtype == gtk.gdk.Pixbuf:
339             renderer = gtk.CellRendererPixbuf()
340             column = gtk.TreeViewColumn(columnName, renderer, pixbuf=column)
341             env.debug("Created a PixBuf column for " + columnName)
342         elif objtype == gobject.TYPE_STRING or objtype == gobject.TYPE_INT:
343             renderer = gtk.CellRendererText()
344             column = gtk.TreeViewColumn(columnName,
345                                         renderer,
346                                         text=column,
347                                         editable=ObjectCollection.COLUMN_ROW_EDITABLE)
348             column.set_resizable(True)
349             if editedCallback:
350                 cid = renderer.connect("edited",
351                                        editedCallback,
352                                        column,
353                                        editedCallbackData)
354                 self.__editedCallbacks[columnName] = (cid, renderer)
355                 env.debug("Created a Text column with editing callback for " + columnName)
356             else:
357                 env.debug("Created a Text column without editing callback for " + columnName)
358         else:
359             print "Warning, unsupported type for column ", columnName
360             return
361         column.set_reorderable(True)
362         self._viewWidget.insert_column(column, location)
363         self.__createdColumns[columnName] = column
364         return column
365
366     def __removeColumn(self, columnName):
367         column = self.__createdColumns[columnName]
368         self._viewWidget.remove_column(column)
369         if columnName in self.__editedCallbacks:
370             (cid, renderer) = self.__editedCallbacks[columnName]
371             renderer.disconnect(cid)
372             del self.__editedCallbacks[columnName]
373         del self.__createdColumns[columnName]
374         column.destroy()
375         env.debug("Removed column " + columnName)
376
377     def __removeColumnsAndUpdateLocation(self, columnNames=None):
378         # Remove columns and store their relative locations for next time
379         # they are re-created.
380         columnLocation = 0
381         for column in self._viewWidget.get_columns():
382             columnName = column.get_title()
383             # TODO Store the column width and reuse it when the column is
384             #      recreated. I don't know how to store the width since
385             #      column.get_width() return correct values for columns
386             #      containing a gtk.CellRendererPixbuf but only 0 for all
387             #      columns containing a gtk.CellRendererText. It is probably
388             #      a bug in gtk och pygtk. I have not yet reported the bug.
389             if columnNames is None or columnName in columnNames:
390                 if columnName in self.__createdColumns:
391                     self.__removeColumn(columnName)
392                     self.__userChosenColumns[columnName] = columnLocation
393             columnLocation += 1
394
395     def __moveListItem(self, lst, currentIndex, newIndex):
396         if currentIndex == newIndex:
397             return lst
398         if currentIndex < newIndex:
399             newIndex -= 1
400         movingChild = lst[currentIndex]
401         del lst[currentIndex]
402         return lst[:newIndex] + [movingChild] + lst[newIndex:]