~bratsche/decibel-audio-player/notifications

« back to all changes in this revision

Viewing changes to src/gui/listview.py

  • Committer: Bazaar Package Importer
  • Date: 2008-11-07 02:15:10 UTC
  • Revision ID: jamesw@ubuntu.com-20081107021510-kur5qpiammaiaxad
Tags: upstream-ubuntu-0.05.2
Import upstream version 0.05.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Author: Ingelrest François (Athropos@gmail.com)
 
4
#
 
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.
 
9
#
 
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.
 
14
#
 
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
 
18
 
 
19
import gtk, random
 
20
 
 
21
from gtk     import gdk
 
22
from gobject import signal_new, TYPE_INT, TYPE_LONG, TYPE_PYOBJECT, TYPE_NONE, SIGNAL_RUN_LAST
 
23
 
 
24
 
 
25
# Identifiers of accepted DND targets
 
26
(
 
27
    DND_INTERNAL,
 
28
    DND_EXTERNAL_URI
 
29
) = range(2)
 
30
 
 
31
 
 
32
# DND targets that this tree may accept
 
33
DND_TARGETS = {
 
34
                DND_INTERNAL:     ('internal',      gtk.TARGET_SAME_WIDGET, DND_INTERNAL),
 
35
                DND_EXTERNAL_URI: ('text/uri-list',                      0, DND_EXTERNAL_URI)
 
36
              }
 
37
 
 
38
 
 
39
# Custom signals
 
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))
 
43
 
 
44
 
 
45
class ListView(gtk.TreeView):
 
46
 
 
47
 
 
48
    def __init__(self, columns):
 
49
        """ Constructor """
 
50
        gtk.TreeView.__init__(self)
 
51
 
 
52
        self.selection = self.get_selection()
 
53
 
 
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)
 
58
 
 
59
        self.set_headers_clickable(True)
 
60
 
 
61
        # Create the columns
 
62
        nbEntries = 0
 
63
        dataTypes = []
 
64
        for (title, renderers, sortIndex, expandable) in columns:
 
65
            if title is None:
 
66
                for (renderer, type) in renderers:
 
67
                    nbEntries += 1
 
68
                    dataTypes.append(type)
 
69
            else:
 
70
                column = gtk.TreeViewColumn(title)
 
71
                column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
 
72
                column.set_expand(expandable)
 
73
                # FIXME
 
74
                # From the PyGTK doc: "This means that once the model has been sorted, it can't go back to the default state"
 
75
                # Is this a joke???
 
76
                # column.set_sort_column_id(sortIndex)
 
77
                self.append_column(column)
 
78
 
 
79
                for (renderer, type) in renderers:
 
80
                    nbEntries += 1
 
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)
 
85
 
 
86
        # Create the ListStore associated with this tree
 
87
        self.store = gtk.ListStore(*dataTypes)
 
88
        self.set_model(self.store)
 
89
 
 
90
        # Drag'n'drop management
 
91
        self.dndContext  = None
 
92
        self.dndEnabled  = False
 
93
        self.dndStartPos = None
 
94
        self.motionEvtId = None
 
95
 
 
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)
 
101
 
 
102
        # Mark management
 
103
        self.markedRow = None
 
104
 
 
105
        # Show the list
 
106
        self.show()
 
107
 
 
108
    # --== Miscellaneous ==--
 
109
 
 
110
 
 
111
    def shuffle(self):
 
112
        """ Shuffle the content of the list """
 
113
        order = range(len(self.store))
 
114
        random.shuffle(order)
 
115
 
 
116
        # Move the mark if needed
 
117
        if self.hasMark():
 
118
            for i in xrange(len(order)):
 
119
                if order[i] == self.markedRow:
 
120
                    self.markedRow = i
 
121
                    break
 
122
 
 
123
        self.store.reorder(order)
 
124
        self.emit('listview-modified')
 
125
 
 
126
 
 
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]]
 
130
 
 
131
 
 
132
    # --== Mark management ==--
 
133
 
 
134
 
 
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
 
138
 
 
139
 
 
140
    def clearMark(self):
 
141
        """ Remove the mark """
 
142
        self.markedRow = None
 
143
 
 
144
 
 
145
    def hasMark(self):
 
146
        """ True if a mark has been set """
 
147
        return self.markedRow is not None
 
148
 
 
149
 
 
150
    def getMark(self):
 
151
        """ Return the index of the marked row """
 
152
        return self.markedRow
 
153
 
 
154
 
 
155
    # --== Retrieving content ==--
 
156
 
 
157
 
 
158
    def getCount(self):
 
159
        """ Return how many rows are stored in the list """
 
160
        return len(self.store)
 
161
 
 
162
 
 
163
    def getRow(self, rowIndex):
 
164
        """ Return the given row """
 
165
        return tuple(self.store[rowIndex])
 
166
 
 
167
 
 
168
    def getAllRows(self):
 
169
        """ Return all rows """
 
170
        return [tuple(row) for row in self.store]
 
171
 
 
172
 
 
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)
 
176
 
 
177
 
 
178
    # --== Adding/removing content ==--
 
179
 
 
180
 
 
181
    def clear(self):
 
182
        """ Remove all rows from the list """
 
183
        self.clearMark()
 
184
        self.store.clear()
 
185
 
 
186
 
 
187
    def getSelectedRowsCount(self):
 
188
        """ Return how many rows are currently selected """
 
189
        return self.selection.count_selected_rows()
 
190
 
 
191
 
 
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]]
 
195
 
 
196
 
 
197
    def getFirstSelectedRowIndex(self):
 
198
        """ Return the index of the first selected row """
 
199
        return self.selection.get_selected_rows()[1][0][0]
 
200
 
 
201
 
 
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)
 
205
 
 
206
 
 
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
 
212
            if self.hasMark():
 
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')
 
221
 
 
222
 
 
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()
 
233
 
 
234
 
 
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)
 
240
 
 
241
        # Insert the new rows
 
242
        self.freeze_child_notify()
 
243
        if position is None:
 
244
            for row in rows:
 
245
                self.store.append(row)
 
246
        else:
 
247
            for row in rows:
 
248
                self.store.insert(position, row)
 
249
                position += 1
 
250
        self.thaw_child_notify()
 
251
        self.emit('listview-modified')
 
252
 
 
253
 
 
254
    # --== D'n'D management ==--
 
255
 
 
256
 
 
257
    def enableDND(self):
 
258
        """ Enable Drag'n'Drop for this list """
 
259
        self.dndEnabled = True
 
260
        self.enable_model_drag_dest(DND_TARGETS.values(), gdk.ACTION_DEFAULT)
 
261
 
 
262
 
 
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
 
266
 
 
267
 
 
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))
 
272
 
 
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]
 
275
 
 
276
        self.freeze_child_notify()
 
277
        for srcIter in iterList:
 
278
            srcPath = self.store.get_path(srcIter)[0]
 
279
 
 
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])
 
282
 
 
283
            self.store.remove(srcIter)
 
284
            dstPath = self.store.get_path(dstIter)[0]
 
285
 
 
286
            if srcPath > dstPath:
 
287
                path += 1
 
288
 
 
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')
 
295
 
 
296
 
 
297
    # --== GTK Handlers ==--
 
298
 
 
299
 
 
300
    def onButtonPressed(self, tree, event):
 
301
        """ A mouse button has been pressed """
 
302
        retVal   = False
 
303
        pathInfo = self.get_path_at_pos(int(event.x), int(event.y))
 
304
 
 
305
        if pathInfo is None: path = None
 
306
        else:                path = pathInfo[0]
 
307
 
 
308
        if event.button == 1 or event.button == 3:
 
309
            if path is None:
 
310
                self.selection.unselect_all()
 
311
            else:
 
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)
 
315
 
 
316
                if event.state == 0 and not self.selection.path_is_selected(path):
 
317
                    self.selection.unselect_all()
 
318
                    self.selection.select_path(path)
 
319
                else:
 
320
                    retVal = (event.state == 0 and self.getSelectedRowsCount() > 1 and self.selection.path_is_selected(path))
 
321
 
 
322
        self.emit('listview-button-pressed', event, path)
 
323
 
 
324
        return retVal
 
325
 
 
326
 
 
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
 
333
 
 
334
            if self.dndEnabled:
 
335
                self.enable_model_drag_dest(DND_TARGETS.values(), gdk.ACTION_DEFAULT)
 
336
 
 
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])
 
342
 
 
343
 
 
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)
 
348
 
 
349
 
 
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)
 
354
 
 
355
 
 
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)
 
360
 
 
361
 
 
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))
 
365
 
 
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)
 
368
        else:
 
369
            self.enable_model_drag_dest(DND_TARGETS.values(), gdk.ACTION_DEFAULT)