~jwiltshire/pogo/debian

« back to all changes in this revision

Viewing changes to src/modules/Tracktree.py

  • Committer: Jonathan Wiltshire
  • Date: 2010-12-20 23:52:57 UTC
  • Revision ID: git-v1:e24ab7d692aa9fecd89514fbd769b83b9db2dd55
Tags: upstream/0.3
Imported Upstream version 0.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Authors: Ingelrest François (Francois.Ingelrest@gmail.com)
 
4
#         Jendrik Seipp (jendrikseipp@web.de)
 
5
#
 
6
# This program is free software; you can redistribute it and/or modify
 
7
# it under the terms of the GNU General Public License as published by
 
8
# the Free Software Foundation; either version 2 of the License, or
 
9
# (at your option) any later version.
 
10
#
 
11
# This program is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
# GNU Library General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License
 
17
# along with this program; if not, write to the Free Software
 
18
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
19
 
 
20
import os
 
21
import traceback
 
22
import logging
 
23
 
 
24
import gtk, gobject
 
25
 
 
26
import gui, media, modules, tools
 
27
 
 
28
from gui             import fileChooser
 
29
from tools           import consts, icons, prefs, pickleLoad, pickleSave, log
 
30
from gettext         import gettext as _
 
31
from gobject         import TYPE_STRING, TYPE_INT, TYPE_PYOBJECT
 
32
from gui.widgets     import TrackTreeView
 
33
 
 
34
MOD_INFO = ('Tracktree', 'Tracktree', '', [], True, False)
 
35
 
 
36
# The format of a row in the treeview
 
37
(
 
38
    ROW_ICO,    # Item icon
 
39
    ROW_NAME,   # Item name
 
40
    ROW_TRK,    # The track object
 
41
) = range(3)
 
42
 
 
43
 
 
44
PREFS_DEFAULT_REPEAT_STATUS      = False
 
45
 
 
46
# Internal d'n'd (reordering)
 
47
DND_REORDERING_ID   = 1024
 
48
DND_INTERNAL_TARGET = ('extListview-internal', gtk.TARGET_SAME_WIDGET, DND_REORDERING_ID)
 
49
 
 
50
 
 
51
class Tracktree(modules.Module):
 
52
    """ This module manages the tracklist """
 
53
 
 
54
    def __init__(self):
 
55
        """ Constructor """
 
56
        handlers = {
 
57
                        consts.MSG_CMD_NEXT:              self.jumpToNext,
 
58
                        consts.MSG_EVT_PAUSED:            lambda: self.onPausedToggled(icons.pauseMenuIcon()),
 
59
                        consts.MSG_EVT_STOPPED:           self.onStopped,
 
60
                        consts.MSG_EVT_UNPAUSED:          lambda: self.onPausedToggled(icons.playMenuIcon()),
 
61
                        consts.MSG_CMD_PREVIOUS:          self.jumpToPrevious,
 
62
                        consts.MSG_EVT_NEED_BUFFER:       self.onBufferingNeeded,
 
63
                        consts.MSG_EVT_APP_STARTED:       self.onAppStarted,
 
64
                        consts.MSG_EVT_APP_QUIT:          self.onAppQuit,
 
65
                        consts.MSG_CMD_TOGGLE_PAUSE:      self.togglePause,
 
66
                        consts.MSG_CMD_TRACKLIST_DEL:     self.remove,
 
67
                        consts.MSG_CMD_TRACKLIST_ADD:     self.insert,
 
68
                        consts.MSG_CMD_TRACKLIST_SET:     self.set,
 
69
                        consts.MSG_CMD_TRACKLIST_CLR:     lambda: self.set(None, None),
 
70
                        consts.MSG_EVT_TRACK_ENDED_OK:    lambda: self.onTrackEnded(False),
 
71
                        #consts.MSG_CMD_TRACKLIST_REPEAT:  self.setRepeat,
 
72
                        consts.MSG_EVT_TRACK_ENDED_ERROR: lambda: self.onTrackEnded(True),
 
73
                        #consts.MSG_CMD_TRACKLIST_SHUFFLE: self.shuffleTracklist,
 
74
                        consts.MSG_CMD_FILE_EXPLORER_DRAG_BEGIN: self.onDragBegin,
 
75
                   }
 
76
 
 
77
        modules.Module.__init__(self, handlers)
 
78
        
 
79
        
 
80
    def getTreeDump(self, path=None):
 
81
        """ Recursively dump the given tree starting at path (None for the root of the tree) """
 
82
        list = []
 
83
 
 
84
        for child in self.tree.iterChildren(path):
 
85
            row = self.tree.getRow(child)
 
86
 
 
87
            if self.tree.getNbChildren(child) == 0: grandChildren = None
 
88
            #elif self.tree.row_expanded(child):     grandChildren = self.getTreeDump(child)
 
89
            #else:                                   grandChildren = []
 
90
            else: grandChildren = self.getTreeDump(child)
 
91
            
 
92
            name = row[ROW_NAME].replace('<b>', '').replace('</b>', '')
 
93
 
 
94
            list.append([(name, row[ROW_TRK]), grandChildren])
 
95
 
 
96
        return list
 
97
 
 
98
 
 
99
    def restoreTreeDump(self, dump, parent=None):
 
100
        """ Recursively restore the dump under the given parent (None for the root of the tree) """
 
101
        if not type(dump) == list:
 
102
            # This dump is from version 0.1 where we saved the TrackDir
 
103
            return
 
104
            
 
105
        for item in dump:
 
106
            (name, track) = item[0]
 
107
 
 
108
            if track:
 
109
                self.tree.appendRow((icons.nullMenuIcon(), name, track), parent)
 
110
            else:
 
111
                newNode = self.tree.appendRow((icons.mediaDirMenuIcon(), name, None), parent)
 
112
 
 
113
                if item[1] is not None:
 
114
                    if len(item[1]) != 0:
 
115
                        # We must expand the row before adding the real children, but this works only if there is already at least one child
 
116
                        self.restoreTreeDump(item[1], newNode)
 
117
        
 
118
        
 
119
    def getTracks(self, rows):
 
120
        ## Unused
 
121
        tracks = []
 
122
        for row in rows:
 
123
            track = self.tree.getTrack(row)
 
124
            if track:
 
125
                tracks.append(track)
 
126
        return tracks
 
127
        
 
128
    
 
129
    def getTrackDir(self, root=None):
 
130
        flat = False if root else True
 
131
        name = self.tree.getLabel(root) if root else 'playtree'
 
132
        name = name.replace('<b>', '').replace('</b>', '')
 
133
        trackdir = media.TrackDir(name=name, flat=flat)
 
134
        
 
135
        for iter in self.tree.iter_children(root):
 
136
            track = self.tree.getTrack(iter)
 
137
            if track:
 
138
                trackdir.tracks.append(track)
 
139
            else:
 
140
                subdir = self.getTrackDir(iter)
 
141
                trackdir.subdirs.append(subdir)
 
142
        
 
143
        return trackdir
 
144
        
 
145
 
 
146
    def __getNextTrackIter(self):
 
147
        """ Return the index of the next track, or -1 if there is none """
 
148
        next = None
 
149
        while True:
 
150
            next = self.tree.get_next_iter(next)
 
151
            if not next:
 
152
                return None
 
153
            
 
154
            # Check track
 
155
            error = self.tree.getItem(next, ROW_ICO) == icons.errorMenuIcon()
 
156
            track = self.tree.getItem(next, ROW_TRK)
 
157
            if track and not error:
 
158
                # Row is not a directory
 
159
                return next
 
160
 
 
161
 
 
162
    def __hasNextTrack(self):
 
163
        """ Return whether there is a next track """
 
164
        return self.__getNextTrackIter() is not None
 
165
 
 
166
 
 
167
    def __getPreviousTrackIter(self):
 
168
        """ Return the index of the previous track, or -1 if there is none """
 
169
        prev = None
 
170
        while True:
 
171
            prev = self.tree.get_prev_iter(prev)
 
172
            if not prev:
 
173
                return None
 
174
            
 
175
            # Check track
 
176
            error = self.tree.getItem(prev, ROW_ICO) == icons.errorMenuIcon()
 
177
            track = self.tree.getItem(prev, ROW_TRK)
 
178
            if track and not error:
 
179
                # Row is not a directory
 
180
                return prev
 
181
 
 
182
 
 
183
    def __hasPreviousTrack(self):
 
184
        """ Return whether there is a previous track """
 
185
        return self.__getPreviousTrackIter() is not None
 
186
 
 
187
 
 
188
    def jumpToNext(self):
 
189
        """ Jump to the next track, if any """
 
190
        where = self.__getNextTrackIter()
 
191
        if where:
 
192
            self.jumpTo(where)
 
193
 
 
194
 
 
195
    def jumpToPrevious(self):
 
196
        """ Jump to the previous track, if any """
 
197
        where = self.__getPreviousTrackIter()
 
198
        if where:
 
199
            self.jumpTo(where)
 
200
            
 
201
            
 
202
    def set_track_playing(self, iter, playing):
 
203
        if not iter:
 
204
            return
 
205
        track = self.tree.getTrack(iter)
 
206
        if not track:
 
207
            return
 
208
            
 
209
        for parent in self.tree.get_all_parents(iter):
 
210
            parent_label = self.tree.getLabel(parent)
 
211
            parent_label = tools.htmlUnescape(parent_label)
 
212
            is_bold = parent_label.startswith('<b>') and parent_label.endswith('</b>')
 
213
            if playing and not is_bold:
 
214
                parent_label = tools.htmlEscape(parent_label)
 
215
                self.tree.setLabel(parent, '<b>%s</b>' % parent_label)
 
216
            elif not playing and is_bold:
 
217
                parent_label = tools.htmlEscape(parent_label[3:-4])
 
218
                self.tree.setLabel(parent, parent_label)
 
219
            
 
220
        parent = self.tree.store.iter_parent(iter)
 
221
        parent_label = self.tree.getLabel(parent) if parent else None
 
222
        label = track.get_label(parent_label=parent_label, playing=playing)
 
223
        if playing:
 
224
            self.tree.setLabel(iter, label)
 
225
            self.tree.setItem(iter, ROW_ICO, icons.playMenuIcon())
 
226
            self.tree.expand(iter)
 
227
        else:
 
228
            self.tree.setLabel(iter, label)
 
229
            icon = self.tree.getItem(iter, ROW_ICO)
 
230
            has_error = (icon == icons.errorMenuIcon())
 
231
            is_dir = (icon == icons.mediaDirMenuIcon())
 
232
            if not is_dir and not has_error:
 
233
                self.tree.setItem(iter, ROW_ICO, icons.nullMenuIcon())
 
234
        
 
235
 
 
236
    def jumpTo(self, iter, sendPlayMsg=True):
 
237
        """ Jump to the track located at the given iter """
 
238
        if not iter:
 
239
            return
 
240
            
 
241
        mark = self.tree.getMark()
 
242
        if mark:
 
243
            self.set_track_playing(mark, False)
 
244
            
 
245
        self.tree.setMark(iter)
 
246
        self.tree.scroll(iter)
 
247
            
 
248
        # Check track
 
249
        track = self.tree.getTrack(iter)
 
250
        if not track:
 
251
            # Row may be a directory
 
252
            self.jumpTo(self.__getNextTrackIter())
 
253
            return
 
254
            
 
255
        self.set_track_playing(iter, True)
 
256
 
 
257
        if sendPlayMsg:
 
258
            modules.postMsg(consts.MSG_CMD_PLAY, {'uri': track.getURI()})
 
259
 
 
260
        modules.postMsg(consts.MSG_EVT_NEW_TRACK,   {'track': track})
 
261
        modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
 
262
        
 
263
        
 
264
    def insert(self, tracks, target=None, drop_mode=None, playNow=True, highlight=False):
 
265
        if type(tracks) == list:
 
266
            trackdir = media.TrackDir(None, flat=True)
 
267
            trackdir.tracks = tracks
 
268
            tracks = trackdir
 
269
            
 
270
        self.tree.get_selection().unselect_all()
 
271
        self.insertDir(tracks, target, drop_mode, highlight)
 
272
        self.onListModified()
 
273
        return
 
274
            
 
275
        # TODO: playNow wanted? Buggy in current state
 
276
        if playNow:
 
277
            if parent is None:
 
278
                dest = self.tree.get_last_root()
 
279
            else:
 
280
                dest = self.tree.get_last_child_iter(parent)
 
281
            self.jumpTo(dest)
 
282
            
 
283
            
 
284
    def insertDir(self, trackdir, target=None, drop_mode=None, highlight=False):
 
285
        '''
 
286
        Insert a directory recursively, return the iter of the first
 
287
        added element
 
288
        '''
 
289
        model = self.tree.store
 
290
        if trackdir.flat:
 
291
            new = target
 
292
        else:
 
293
            string = trackdir.dirname.replace('_', ' ')
 
294
            string = tools.htmlEscape(string)
 
295
            source_row = (icons.mediaDirMenuIcon(), string, None)
 
296
            
 
297
            new = self.tree.insert(target, source_row, drop_mode)
 
298
            drop_mode = gtk.TREE_VIEW_DROP_INTO_OR_AFTER
 
299
            if highlight:
 
300
                self.tree.select(new)
 
301
        
 
302
        dest = new
 
303
        for index, subdir in enumerate(trackdir.subdirs):
 
304
            drop = drop_mode if index == 0 else gtk.TREE_VIEW_DROP_AFTER
 
305
            dest = self.insertDir(subdir, dest, drop, highlight)
 
306
            
 
307
        dest = new
 
308
        for index, track in enumerate(trackdir.tracks):
 
309
            drop = drop_mode if index == 0 else gtk.TREE_VIEW_DROP_AFTER
 
310
            highlight &= trackdir.flat
 
311
            dest = self.insertTrack(track, dest, drop, highlight)
 
312
            
 
313
        if not trackdir.flat:
 
314
            # Open albums on the first layer
 
315
            if target is None or model.iter_depth(new) == 0:
 
316
                self.tree.expand(new)
 
317
        
 
318
        return new
 
319
        
 
320
        
 
321
    def insertTrack(self, track, target=None, drop_mode=None, highlight=False):
 
322
        '''
 
323
        Insert a new track into the tracktree under parentPath
 
324
        '''
 
325
        length = track.getLength()
 
326
        self.playtime += length
 
327
        
 
328
        name = track.get_label()
 
329
        
 
330
        row = (icons.nullMenuIcon(), name, track)
 
331
        new_iter = self.tree.insert(target, row, drop_mode)
 
332
        parent = self.tree.store.iter_parent(new_iter)
 
333
        if parent:
 
334
            # adjust track label to parent
 
335
            parent_label = self.tree.getLabel(parent)
 
336
            new_label = track.get_label(parent_label)
 
337
            self.tree.setLabel(new_iter, new_label)
 
338
        if highlight:
 
339
            self.tree.select(new_iter)
 
340
        return new_iter
 
341
 
 
342
 
 
343
    def set(self, tracks, playNow):
 
344
        """ Replace the tracklist, clear it if tracks is None """
 
345
        self.playtime = 0
 
346
        
 
347
        if type(tracks) == list:
 
348
            trackdir = media.TrackDir(None, flat=True)
 
349
            trackdir.tracks = tracks
 
350
            tracks = trackdir
 
351
 
 
352
        if self.tree.hasMark() and ((not playNow) or (tracks is None) or tracks.empty()):
 
353
            modules.postMsg(consts.MSG_CMD_STOP)
 
354
 
 
355
        self.tree.clear()
 
356
        
 
357
        if tracks is not None and not tracks.empty():
 
358
            self.insert(tracks)
 
359
            
 
360
        self.tree.collapse_all()
 
361
            
 
362
        self.onListModified()
 
363
 
 
364
 
 
365
    def savePlaylist(self):
 
366
        """ Save the current tracklist to a playlist """
 
367
        outFile = fileChooser.save(self.window, _('Save playlist'), 'playlist.m3u')
 
368
 
 
369
        if outFile is not None:
 
370
            allFiles = [row[ROW_TRK].getFilePath() for row in self.tree.iterAllRows()]
 
371
            media.playlist.save(allFiles, outFile)
 
372
 
 
373
 
 
374
    def remove(self, iter=None):
 
375
        """ Remove the given track, or the selection if iter is None """
 
376
        hadMark = self.tree.hasMark()
 
377
        
 
378
        iters = [iter] if iter else list(self.tree.iterSelectedRows())
 
379
        
 
380
        # reverse list, so that we remove children before their fathers
 
381
        for iter in reversed(iters):
 
382
            track = self.tree.getTrack(iter)
 
383
            if track:
 
384
                self.playtime -= track.getLength()
 
385
            self.tree.removeRow(iter)
 
386
 
 
387
        self.tree.selection.unselect_all()
 
388
 
 
389
        if hadMark and not self.tree.hasMark():
 
390
            modules.postMsg(consts.MSG_CMD_STOP)
 
391
            
 
392
        self.onListModified()
 
393
 
 
394
 
 
395
    def onShowPopupMenu(self, tree, button, time, path):
 
396
        """ The index parameter may be None """
 
397
        if path is None:
 
398
            iter = None
 
399
        else:
 
400
            iter = tree.store.get_iter(path)
 
401
        
 
402
        popup = gtk.Menu()
 
403
 
 
404
        # Remove
 
405
        remove = gtk.ImageMenuItem(gtk.STOCK_REMOVE)
 
406
        popup.append(remove)
 
407
 
 
408
        if iter is None:
 
409
            remove.set_sensitive(False)
 
410
        else:
 
411
            remove.connect('activate', lambda item: self.remove())
 
412
 
 
413
        #popup.append(gtk.SeparatorMenuItem())
 
414
 
 
415
        # Clear
 
416
        clear = gtk.ImageMenuItem(_('Clear Playlist'))
 
417
        clear.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU))
 
418
        popup.append(clear)
 
419
 
 
420
        if len(tree.store) == 0:
 
421
            clear.set_sensitive(False)
 
422
        else:
 
423
            clear.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR))
 
424
 
 
425
        popup.show_all()
 
426
        popup.popup(None, None, None, button, time)
 
427
 
 
428
 
 
429
    def togglePause(self):
 
430
        """ Start playing if not already playing """
 
431
        if len(self.tree) != 0 and not self.tree.hasMark():
 
432
            if self.tree.selection.count_selected_rows() > 0:
 
433
                model, sel_rows_list = self.tree.selection.get_selected_rows()
 
434
                first_sel_iter = self.tree.store.get_iter(sel_rows_list[0])
 
435
                self.jumpTo(first_sel_iter)
 
436
            else:
 
437
                self.jumpTo(self.tree.get_first_iter())
 
438
                
 
439
                
 
440
    def restore_expanded_rows(self):
 
441
        for path in self.tree.expanded_rows:
 
442
            self.tree.expand(path)
 
443
        self.tree.expanded_rows = None
 
444
 
 
445
 
 
446
    # --== Message handlers ==--
 
447
 
 
448
 
 
449
    def onAppStarted(self):
 
450
        """ This is the real initialization function, called when the module has been loaded """
 
451
        wTree                  = tools.prefs.getWidgetsTree()
 
452
        self.playtime          = 0
 
453
        self.bufferedTrack     = None
 
454
        self.previousTracklist = None
 
455
        # Retrieve widgets
 
456
        self.window     = wTree.get_widget('win-main')
 
457
        self.btnClear   = wTree.get_widget('btn-tracklistClear')
 
458
        self.btnRepeat  = wTree.get_widget('btn-tracklistRepeat')
 
459
        self.btnShuffle = wTree.get_widget('btn-tracklistShuffle')
 
460
 
 
461
        columns = (('',   [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf), (gtk.CellRendererText(), TYPE_STRING)], True),
 
462
                   (None, [(None, TYPE_PYOBJECT)], False),
 
463
                  )
 
464
        
 
465
        self.tree = TrackTreeView(columns, use_markup=True)
 
466
        self.tree.enableDNDReordering()
 
467
        #self.tree.setDNDSources([consts.DND_TARGETS[consts.DND_POGO_TRACKS]])
 
468
        self.tree.setDNDSources([DND_INTERNAL_TARGET])
 
469
        
 
470
        wTree.get_widget('scrolled-tracklist').add(self.tree)
 
471
        
 
472
        # GTK handlers         
 
473
        self.tree.connect('exttreeview-button-pressed', self.onMouseButton)
 
474
        self.tree.connect('tracktreeview-dnd', self.onDND)
 
475
        self.tree.connect('key-press-event', self.onKeyboard)
 
476
        #self.tree.connect('extlistview-modified', self.onListModified)
 
477
        #self.tree.connect('button-pressed', self.onButtonPressed)
 
478
        #self.btnClear.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR))
 
479
        #self.btnRepeat.connect('toggled', self.onButtonRepeat)
 
480
        #self.btnShuffle.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE))
 
481
        # Restore preferences
 
482
        #self.btnRepeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS))
 
483
        # Set icons
 
484
        #wTree.get_widget('img-repeat').set_from_icon_name('stock_repeat', gtk.ICON_SIZE_BUTTON)
 
485
        #wTree.get_widget('img-shuffle').set_from_icon_name('stock_shuffle', gtk.ICON_SIZE_BUTTON)
 
486
        
 
487
        # Populate the playlist with commandline args or the saved playlist
 
488
        (options, args)    = prefs.getCmdLine()
 
489
        
 
490
        self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist')
 
491
 
 
492
        if len(args) != 0:
 
493
            log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME])
 
494
            # make paths absolute
 
495
            paths = map(os.path.abspath, args)
 
496
            print 'Appending to the playlist:'
 
497
            print '\n'.join(paths)
 
498
            modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': media.getTracks(paths), 'playNow': True})
 
499
        else:
 
500
            dump = None
 
501
            if os.path.exists(self.savedPlaylist):
 
502
                try:
 
503
                    dump = pickleLoad(self.savedPlaylist)
 
504
                except:
 
505
                    msg = '[%s] Unable to restore playlist from %s\n\n%s'
 
506
                    log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME], 
 
507
                                    self.savedPlaylist, traceback.format_exc()))
 
508
            
 
509
            if dump:
 
510
                self.restoreTreeDump(dump)
 
511
                log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME])
 
512
                self.tree.collapse_all()
 
513
                self.onListModified()
 
514
        
 
515
    
 
516
    def onAppQuit(self):
 
517
        """ The module is going to be unloaded """
 
518
        dump = self.getTreeDump()
 
519
        logging.info('Saving playlist')
 
520
        pickleSave(self.savedPlaylist, dump)
 
521
 
 
522
 
 
523
    def onTrackEnded(self, withError):
 
524
        """ The current track has ended, jump to the next one if any """
 
525
        current_iter = self.tree.getMark()
 
526
 
 
527
        # If an error occurred with the current track, flag it as such
 
528
        if withError and current_iter:
 
529
            self.tree.setItem(current_iter, ROW_ICO, icons.errorMenuIcon())
 
530
 
 
531
        # Find the next 'playable' track (not already flagged)
 
532
        next = self.__getNextTrackIter()
 
533
        if next:
 
534
            send_play_msg = True
 
535
            if current_iter:
 
536
                track_name = self.tree.getTrack(current_iter).getURI()
 
537
                send_play_msg = (track_name != self.bufferedTrack)
 
538
            self.jumpTo(next, sendPlayMsg=send_play_msg)
 
539
            self.bufferedTrack = None
 
540
            return
 
541
 
 
542
        self.bufferedTrack = None
 
543
        modules.postMsg(consts.MSG_CMD_STOP)
 
544
 
 
545
 
 
546
    def onBufferingNeeded(self):
 
547
        """ The current track is close to its end, so we try to buffer the next one to avoid gaps """
 
548
        where = self.__getNextTrackIter()
 
549
        if where:
 
550
            self.bufferedTrack = self.tree.getItem(where, ROW_TRK).getURI()
 
551
            modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.bufferedTrack})
 
552
 
 
553
 
 
554
    def onStopped(self):
 
555
        """ Playback has been stopped """
 
556
        if self.tree.hasMark():
 
557
            currTrack = self.tree.getMark()
 
558
            self.set_track_playing(currTrack, False)
 
559
            self.tree.clearMark()
 
560
 
 
561
 
 
562
    def onPausedToggled(self, icon):
 
563
        """ Switch between paused and unpaused """
 
564
        if self.tree.hasMark():
 
565
            self.tree.setItem(self.tree.getMark(), ROW_ICO, icon)
 
566
            
 
567
            
 
568
    def onDragBegin(self, paths):
 
569
        dir_selected = any(map(os.path.isdir, paths))
 
570
        self.tree.dir_selected = dir_selected
 
571
        if dir_selected:
 
572
            # Save expanded rows
 
573
            #def expanded(treeview, path):
 
574
            #    row = self.tree.store.get_iter(path)
 
575
            #    self.tree.expanded_rows.append(row)
 
576
            
 
577
            #self.tree.expanded_rows = []
 
578
            #self.tree.map_expanded_rows(expanded)
 
579
        
 
580
            self.tree.collapse_all()
 
581
 
 
582
 
 
583
    # --== GTK handlers ==--
 
584
            
 
585
        
 
586
    def onMouseButton(self, tree, event, path):
 
587
        """ A mouse button has been pressed """
 
588
        if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS and path is not None:
 
589
            self.jumpTo(self.tree.store.get_iter(path))
 
590
        elif event.button == 3:
 
591
            self.onShowPopupMenu(tree, event.button, event.time, path)
 
592
 
 
593
 
 
594
    def onKeyboard(self, list, event):
 
595
        """ Keyboard shortcuts """
 
596
        keyname = gtk.gdk.keyval_name(event.keyval)
 
597
 
 
598
        if keyname == 'Delete':   self.remove()
 
599
        elif keyname == 'Return': self.jumpTo(self.tree.getFirstSelectedRow())
 
600
        elif keyname == 'space':  modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
 
601
        elif keyname == 'Escape': modules.postMsg(consts.MSG_CMD_STOP)
 
602
        elif keyname == 'Left':   modules.postMsg(consts.MSG_CMD_STEP, {'seconds': -5})
 
603
        elif keyname == 'Right':  modules.postMsg(consts.MSG_CMD_STEP, {'seconds': 5})
 
604
 
 
605
 
 
606
    def onListModified(self):
 
607
        """ Some rows have been added/removed/moved """
 
608
        # Getting the trackdir takes virtually no time, so we can do it on every
 
609
        # paylist change
 
610
        tracks = self.getTrackDir()
 
611
        self.playtime = tracks.get_playtime()
 
612
        
 
613
        modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': tracks, 'playtime': self.playtime})
 
614
 
 
615
        if self.tree.hasMark():
 
616
            modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext':  self.__hasNextTrack()})
 
617
 
 
618
 
 
619
    def onDND(self, list, context, x, y, dragData, dndId, time):
 
620
        """ External Drag'n'Drop """
 
621
        import urllib
 
622
 
 
623
        if dragData.data == '':
 
624
            context.finish(False, False, time)
 
625
            return
 
626
 
 
627
        # A list of filenames, without 'file://' at the beginning
 
628
        if dndId == consts.DND_POGO_URI:
 
629
            tracks = media.getTracks([urllib.url2pathname(uri) for uri in dragData.data.split()])
 
630
        # A list of filenames starting with 'file://'
 
631
        elif dndId == consts.DND_URI:
 
632
            tracks = media.getTracks([urllib.url2pathname(uri)[7:] for uri in dragData.data.split()])
 
633
        # A list of tracks
 
634
        elif dndId == consts.DND_POGO_TRACKS:
 
635
            tracks = [media.track.unserialize(serialTrack) for serialTrack in dragData.data.split('\n')]
 
636
 
 
637
        # dropInfo is tuple (path, drop_pos)
 
638
        dropInfo = list.get_dest_row_at_pos(x, y)
 
639
 
 
640
        # Insert the tracks, but beware of the AFTER/BEFORE mechanism used by GTK
 
641
        if dropInfo is None:
 
642
            self.insert(tracks, highlight=True)
 
643
        else:
 
644
            path, drop_mode = dropInfo
 
645
            iter = self.tree.store.get_iter(path)
 
646
            self.insert(tracks, iter, drop_mode, highlight=True)
 
647
            
 
648
        #self.restore_expanded_rows()
 
649
        
 
650
        # We want to allow dropping tracks only when we are sure that no dir is
 
651
        # selected. This is needed for dnd from nautilus.
 
652
        self.tree.dir_selected = True
 
653
 
 
654
        context.finish(True, False, time)