1
# -*- coding: utf-8 -*-
3
# Author: Ingelrest François (Athropos@gmail.com)
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU Library General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22
from gobject import signal_new, TYPE_INT, TYPE_LONG, TYPE_PYOBJECT, TYPE_NONE, SIGNAL_RUN_LAST
25
# Identifiers of accepted DND targets
32
# DND targets that this tree may accept
34
DND_INTERNAL: ('internal', gtk.TARGET_SAME_WIDGET, DND_INTERNAL),
35
DND_EXTERNAL_URI: ('text/uri-list', 0, DND_EXTERNAL_URI)
40
signal_new('listview-dnd', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (gdk.DragContext, TYPE_INT, TYPE_INT, gtk.SelectionData, TYPE_LONG))
41
signal_new('listview-modified', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, ())
42
signal_new('listview-button-pressed', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (gdk.Event, TYPE_PYOBJECT))
45
class ListView(gtk.TreeView):
48
def __init__(self, columns):
50
gtk.TreeView.__init__(self)
52
self.selection = self.get_selection()
54
# Default configuration for this list
55
self.set_rules_hint(True)
56
self.set_headers_visible(True)
57
self.selection.set_mode(gtk.SELECTION_MULTIPLE)
59
self.set_headers_clickable(True)
64
for (title, renderers, sortIndex, expandable) in columns:
66
for (renderer, type) in renderers:
68
dataTypes.append(type)
70
column = gtk.TreeViewColumn(title)
71
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
72
column.set_expand(expandable)
74
# From the PyGTK doc: "This means that once the model has been sorted, it can't go back to the default state"
76
# column.set_sort_column_id(sortIndex)
77
self.append_column(column)
79
for (renderer, type) in renderers:
81
dataTypes.append(type)
82
column.pack_start(renderer, True)
83
if isinstance(renderer, gtk.CellRendererText): column.add_attribute(renderer, 'text', nbEntries-1)
84
else: column.add_attribute(renderer, 'pixbuf', nbEntries-1)
86
# Create the ListStore associated with this tree
87
self.store = gtk.ListStore(*dataTypes)
88
self.set_model(self.store)
90
# Drag'n'drop management
91
self.dndContext = None
92
self.dndEnabled = False
93
self.dndStartPos = None
94
self.motionEvtId = None
96
self.connect('drag-begin', self.onDragBegin)
97
self.connect('drag-motion', self.onDragMotion)
98
self.connect('button-press-event', self.onButtonPressed)
99
self.connect('drag-data-received', self.onDragDataReceived)
100
self.connect('button-release-event', self.onButtonReleased)
103
self.markedRow = None
108
# --== Miscellaneous ==--
112
""" Shuffle the content of the list """
113
order = range(len(self.store))
114
random.shuffle(order)
116
# Move the mark if needed
118
for i in xrange(len(order)):
119
if order[i] == self.markedRow:
123
self.store.reorder(order)
124
self.emit('listview-modified')
127
def __getIterOnSelectedRows(self):
128
""" Return a list of iterators pointing to the selected rows """
129
return [self.store.get_iter(path) for path in self.selection.get_selected_rows()[1]]
132
# --== Mark management ==--
135
def setMark(self, rowIndex):
136
""" Put the mark on the given row, it will move with the row itself (e.g., D'n'D) """
137
self.markedRow = rowIndex
141
""" Remove the mark """
142
self.markedRow = None
146
""" True if a mark has been set """
147
return self.markedRow is not None
151
""" Return the index of the marked row """
152
return self.markedRow
155
# --== Retrieving content ==--
159
""" Return how many rows are stored in the list """
160
return len(self.store)
163
def getRow(self, rowIndex):
164
""" Return the given row """
165
return tuple(self.store[rowIndex])
168
def getAllRows(self):
169
""" Return all rows """
170
return [tuple(row) for row in self.store]
173
def getItem(self, rowIndex, colIndex):
174
""" Return the value of the given item """
175
return self.store.get_value(self.store.get_iter(rowIndex), colIndex)
178
# --== Adding/removing content ==--
182
""" Remove all rows from the list """
187
def getSelectedRowsCount(self):
188
""" Return how many rows are currently selected """
189
return self.selection.count_selected_rows()
192
def getSelectedRows(self):
193
""" Return all selected row(s) """
194
return [tuple(self.store[path]) for path in self.selection.get_selected_rows()[1]]
197
def getFirstSelectedRowIndex(self):
198
""" Return the index of the first selected row """
199
return self.selection.get_selected_rows()[1][0][0]
202
def setItem(self, rowIndex, colIndex, value):
203
""" Change the value of the given item """
204
self.store.set_value(self.store.get_iter(rowIndex), colIndex, value)
207
def removeSelectedRows(self):
208
""" Remove the selected row(s) """
209
self.freeze_child_notify()
210
for iter in self.__getIterOnSelectedRows():
211
# Move the mark if needed
213
currentPath = self.store.get_path(iter)[0]
214
if currentPath < self.markedRow: self.markedRow -= 1
215
elif currentPath == self.markedRow: self.markedRow = None
216
# Remove the current row
217
if self.store.remove(iter): self.set_cursor(self.store.get_path(iter))
218
elif len(self.store) != 0: self.set_cursor(len(self.store)-1)
219
self.thaw_child_notify()
220
self.emit('listview-modified')
223
def cropSelectedRows(self):
224
""" Remove all rows but the selected ones """
225
pathsList = self.selection.get_selected_rows()[1]
226
self.freeze_child_notify()
227
self.selection.select_all()
228
for path in pathsList:
229
self.selection.unselect_path(path)
230
self.removeSelectedRows()
231
self.selection.select_all()
232
self.thaw_child_notify()
235
def insertRows(self, rows, position=None):
236
""" Insert or append (if position is None) some rows to the list """
237
# Move the mark if needed
238
if self.hasMark() and position is not None and position <= self.markedRow:
239
self.markedRow += len(rows)
241
# Insert the new rows
242
self.freeze_child_notify()
245
self.store.append(row)
248
self.store.insert(position, row)
250
self.thaw_child_notify()
251
self.emit('listview-modified')
254
# --== D'n'D management ==--
258
""" Enable Drag'n'Drop for this list """
259
self.dndEnabled = True
260
self.enable_model_drag_dest(DND_TARGETS.values(), gdk.ACTION_DEFAULT)
263
def __isDropBefore(self, pos):
264
""" Helper function, True if pos is gtk.TREE_VIEW_DROP_BEFORE or gtk.TREE_VIEW_DROP_INTO_OR_BEFORE """
265
return pos == gtk.TREE_VIEW_DROP_BEFORE or pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE
268
def __moveSelectedRows(self, x, y):
269
""" Internal function used for drag'n'drop """
270
iterList = self.__getIterOnSelectedRows()
271
dropInfo = self.get_dest_row_at_pos(int(x), int(y))
273
if dropInfo is None: pos, path = gtk.TREE_VIEW_DROP_INTO_OR_AFTER, len(self.store) - 1
274
else: pos, path = dropInfo[1], dropInfo[0][0]
276
self.freeze_child_notify()
277
for srcIter in iterList:
278
srcPath = self.store.get_path(srcIter)[0]
280
if self.__isDropBefore(pos): dstIter = self.store.insert_before(self.store.get_iter(path), self.store[srcIter])
281
else: dstIter = self.store.insert_after(self.store.get_iter(path), self.store[srcIter])
283
self.store.remove(srcIter)
284
dstPath = self.store.get_path(dstIter)[0]
286
if srcPath > dstPath:
289
if self.markedRow is not None:
290
if srcPath == self.markedRow: self.markedRow = dstPath
291
elif srcPath < self.markedRow and dstPath >= self.markedRow: self.markedRow -= 1
292
elif srcPath > self.markedRow and dstPath <= self.markedRow: self.markedRow += 1
293
self.thaw_child_notify()
294
self.emit('listview-modified')
297
# --== GTK Handlers ==--
300
def onButtonPressed(self, tree, event):
301
""" A mouse button has been pressed """
303
pathInfo = self.get_path_at_pos(int(event.x), int(event.y))
305
if pathInfo is None: path = None
306
else: path = pathInfo[0]
308
if event.button == 1 or event.button == 3:
310
self.selection.unselect_all()
312
if self.dndEnabled and self.motionEvtId is None and event.button == 1:
313
self.dndStartPos = (int(event.x), int(event.y))
314
self.motionEvtId = gtk.TreeView.connect(self, 'motion-notify-event', self.onMouseMotion)
316
if event.state == 0 and not self.selection.path_is_selected(path):
317
self.selection.unselect_all()
318
self.selection.select_path(path)
320
retVal = (event.state == 0 and self.getSelectedRowsCount() > 1 and self.selection.path_is_selected(path))
322
self.emit('listview-button-pressed', event, path)
327
def onButtonReleased(self, tree, event):
328
""" A mouse button has been released """
329
if self.motionEvtId is not None:
330
self.disconnect(self.motionEvtId)
331
self.dndContext = None
332
self.motionEvtId = None
335
self.enable_model_drag_dest(DND_TARGETS.values(), gdk.ACTION_DEFAULT)
337
if event.state == gtk.gdk.BUTTON1_MASK and self.getSelectedRowsCount() > 1:
338
pathInfo = self.get_path_at_pos(int(event.x), int(event.y))
339
if pathInfo is not None:
340
self.selection.unselect_all()
341
self.selection.select_path(pathInfo[0][0])
344
def onMouseMotion(self, tree, event):
345
""" The mouse has been moved """
346
if self.dndContext is None and self.drag_check_threshold(self.dndStartPos[0], self.dndStartPos[1], int(event.x), int(event.y)):
347
self.dndContext = self.drag_begin([DND_TARGETS[DND_INTERNAL]], gtk.gdk.ACTION_COPY, 1, event)
350
def onDragBegin(self, tree, context):
351
""" A drag'n'drop operation has begun """
352
if self.getSelectedRowsCount() == 1: context.set_icon_stock(gtk.STOCK_DND, 0, 0)
353
else: context.set_icon_stock(gtk.STOCK_DND_MULTIPLE, 0, 0)
356
def onDragDataReceived(self, tree, context, x, y, selection, dndId, time):
357
""" Some data has been dropped into the list """
358
if dndId == DND_INTERNAL: self.__moveSelectedRows(x, y)
359
elif dndId == DND_EXTERNAL_URI: self.emit('listview-dnd', context, int(x), int(y), selection, time)
362
def onDragMotion(self, tree, context, x, y, time):
363
""" Prevent rows from being dragged *into* other rows (this is a list, not a tree) """
364
drop = self.get_dest_row_at_pos(int(x), int(y))
366
if drop is not None and (drop[1] == gtk.TREE_VIEW_DROP_INTO_OR_AFTER or drop[1] == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
367
self.enable_model_drag_dest([('invalid-position', 0, -1)], gdk.ACTION_DEFAULT)
369
self.enable_model_drag_dest(DND_TARGETS.values(), gdk.ACTION_DEFAULT)