class Tracklist(modules.Module): """ This module manages the tracklist """ #consts.MSG_CMD_TRACKLIST_SET, def __init__(self): """ Constructor """ modules.Module.__init__(self, (consts.MSG_CMD_TRACKLIST_CLR, consts.MSG_CMD_TRACKLIST_ADD, consts.MSG_CMD_TOGGLE_PAUSE, consts.MSG_CMD_NEXT, consts.MSG_CMD_PREVIOUS, consts.MSG_EVT_NEED_BUFFER, consts.MSG_EVT_STOPPED, consts.MSG_EVT_PAUSED, consts.MSG_EVT_UNPAUSED, consts.MSG_EVT_TRACK_ENDED_OK, consts.MSG_EVT_TRACK_ENDED_ERROR, consts.MSG_CMD_TRACKLIST_SHUFFLE, consts.MSG_EVT_APP_STARTED)) def onAppStarted(self): """ This is the real initialization function, called when the module has been loaded """ wTree = tools.prefs.getWidgetsTree() self.playtime = 0 self.previousTracklist = None # Retrieve widgets self.window = wTree.get_widget('win-main') # self.btnClear = wTree.get_widget('btn-tracklistClear') self.btnRepeat = wTree.get_widget('btn-tracklistRepeat') self.btnShuffle = wTree.get_widget('btn-tracklistShuffle') # self.btnClear.set_sensitive(False) self.btnShuffle.set_sensitive(False) # Create the list and its columns txtLRdr = gtk.CellRendererText() txtRRdr = gtk.CellRendererText() pixbufRdr = gtk.CellRendererPixbuf() txtRRdr.set_property('xalign', 1.0) # 'columns-visibility' may be broken, we should not use it (#311293) visible = tools.prefs.get(__name__, 'columns-visibility-2', PREFS_DEFAULT_COLUMNS_VISIBILITY) columns = (('#', [(pixbufRdr, gtk.gdk.Pixbuf), (txtRRdr, TYPE_INT)], (ROW_NUM, ROW_TIT), False, visible[COL_TRCK_NUM]), (_('Title'), [(txtLRdr, TYPE_STRING)], (ROW_TIT,), True, visible[COL_TITLE]), (_('Artist'), [(txtLRdr, TYPE_STRING)], (ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ARTIST]), (_('Album'), [(txtLRdr, TYPE_STRING)], (ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ALBUM]), (_('Length'), [(txtRRdr, TYPE_INT)], (ROW_LEN,), False, visible[COL_LENGTH]), (_('Genre'), [(txtLRdr, TYPE_STRING)], (ROW_GNR, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_GENRE]), (_('Date'), [(txtLRdr, TYPE_INT)], (ROW_DAT, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_DATE]), (_('Path'), [(txtLRdr, TYPE_STRING)], (ROW_PTH,), False, visible[COL_PATH]), (None, [(None, TYPE_PYOBJECT)], (None,), False, False)) self.list = ExtListView(columns, sortable=True, dndTargets=consts.DND_TARGETS.values(), useMarkup=False, canShowHideColumns=True) self.list.get_column(4).set_cell_data_func(txtRRdr, self.fmtLength) # self.list.enableDNDReordering() wTree.get_widget('scrolled-tracklist').add(self.list) # GTK handlers self.list.connect('extlistview-dnd', self.onDND) self.list.connect('key-press-event', self.onKeyboard) self.list.connect('extlistview-modified', self.onListModified) self.list.connect('extlistview-button-pressed', self.onButtonPressed) self.list.connect('extlistview-column-visibility-changed', self.onColumnVisibilityChanged) # self.btnClear.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR)) self.btnRepeat.connect('toggled', self.onButtonRepeat) self.btnShuffle.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE)) # Restore preferences self.btnRepeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS)) # Set icons wTree.get_widget('img-repeat').set_from_icon_name('stock_repeat', gtk.ICON_SIZE_BUTTON) wTree.get_widget('img-shuffle').set_from_icon_name('stock_shuffle', gtk.ICON_SIZE_BUTTON) def tiraLed(self): gui.errorMsgBox(self.window, _('Operação de Tira Led'), _('Sem deletar a lista dos irmão!')) def getAllFiles(self): return [row[ROW_TRK].getFilePath() for row in self.list.iterAllRows()] def getAllTracks(self): return [row[ROW_TRK] for row in self.list.iterAllRows()] def fmtLength(self, col, cll, mdl, it): cll.set_property('text', tools.sec2str(mdl.get_value(it, ROW_LEN))) def __getNextTrackIdx(self): """ Return the index of the next track, or -1 if there is none """ if self.list.hasMark(): if self.list.getMark() < (len(self.list) - 1): return self.list.getMark() + 1 elif self.btnRepeat.get_active(): return 0 return -1 def __getPreviousTrackIdx(self): """ Return the index of the previous track, or -1 if there is none """ if self.list.hasMark(): if self.list.getMark() > 0: return self.list.getMark() - 1 elif self.btnRepeat.get_active(): return len(self.list) - 1 return -1 def jumpToNext(self): """ Jump to the next track, if any """ where = self.__getNextTrackIdx() if where != -1: self.jumpTo(where) def jumpToPrevious(self): """ Jump to the previous track, if any """ where = self.__getPreviousTrackIdx() if where != -1: self.jumpTo(where) def jumpTo(self, trackIdx): """ Jump to the track located at the given index """ if self.list.hasMark() and self.list.getItem(self.list.getMark(), ROW_ICO) != consts.icoError: self.list.setItem(self.list.getMark(), ROW_ICO, consts.icoNull) self.list.setMark(trackIdx) self.list.scroll_to_cell(trackIdx) self.list.setItem(trackIdx, ROW_ICO, consts.icoPlay) modules.postMsg(consts.MSG_CMD_PLAY, {'uri': self.list.getItem(trackIdx, ROW_TRK).getURI()}) modules.postMsg(consts.MSG_EVT_NEW_TRACK, {'track': self.list.getRow(trackIdx)[ROW_TRK]}) modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__getPreviousTrackIdx() != -1, 'hasNext': self.__getNextTrackIdx() != -1}) def onTrackEnded(self, withError): """ The current track has ended, jump to the next one if any """ currIdx = self.list.getMark() # If an error occurred with the current track, flag it as such if withError: self.list.setItem(currIdx, ROW_ICO, consts.icoError) # Find the next 'playable' track (not already flagged) if self.btnRepeat.get_active(): nbTracks = len(self.list) else: nbTracks = len(self.list) - 1 - currIdx for i in xrange(nbTracks): currIdx = (currIdx + 1) % len(self.list) if self.list.getItem(currIdx, ROW_ICO) != consts.icoError: self.jumpTo(currIdx) return modules.postMsg(consts.MSG_CMD_STOP) def onBufferingNeeded(self): """ The current track is close to its end, so we try to buffer the next one to avoid gaps """ where = self.__getNextTrackIdx() if where != -1: modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.list.getItem(where, ROW_TRK).getURI()}) def insert(self, tracks, position=None): """ Insert some tracks in the tracklist, append them if position is None """ self.previousTracklist = [row[ROW_TRK] for row in self.list.getAllRows()] rows = [[consts.icoNull, track.getNumber(), track.getTitle(), track.getArtist(), track.getExtendedAlbum(), track.getLength(), track.getGenre(), track.getDate(), track.getURI(), track] for track in tracks] for row in rows: self.playtime += row[ROW_LEN] self.list.insertRows(rows, position) def set(self, tracks, playNow): """ Replace the tracklist, clear it if tracks is None """ self.playtime = 0 previousTracklist = [row[ROW_TRK] for row in self.list.getAllRows()] if self.list.hasMark() and not playNow: modules.postMsg(consts.MSG_CMD_STOP) # self.list.clear() self.tiraLed() if tracks is None: modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': [], 'playtime': 0}) else: self.insert(tracks) if playNow and len(self.list) != 0: self.jumpTo(0) self.previousTracklist = previousTracklist def onStopped(self): """ Playback has been stopped """ if self.list.hasMark(): currTrack = self.list.getMark() if self.list.getItem(currTrack, ROW_ICO) != consts.icoError: self.list.setItem(currTrack, ROW_ICO, consts.icoNull) self.list.clearMark() def onPausedToggled(self, icon): """ Switch between paused and unpaused """ if self.list.hasMark(): self.list.setItem(self.list.getMark(), ROW_ICO, icon) def savePlaylist(self): """ Save the current tracklist to a playlist """ destDir = os.path.dirname(self.list.getItem(0, ROW_TRK).getFilePath()) outFile = gui.fileChooser.save(self.window, _('Save playlist'), 'playlist.m3u', destDir) if outFile is not None: media.playlist.save(self.getAllFiles(), outFile) def removeSelection(self, invert=False): """ Remove the selected tracks if invert is False / the unselected tracks if invert is True """ hadMark = self.list.hasMark() selectionPlaytime = sum([row[ROW_LEN] for row in self.list.iterSelectedRows()]) self.previousTracklist = [row[ROW_TRK] for row in self.list.getAllRows()] if invert: self.playtime = selectionPlaytime self.list.cropSelectedRows() else: self.playtime -= selectionPlaytime self.list.removeSelectedRows() if hadMark and not self.list.hasMark(): modules.postMsg(consts.MSG_CMD_STOP) def revertTracklist(self): """ Back to the previous tracklist """ self.set(self.previousTracklist, False) self.previousTracklist = None def shuffleTracklist(self): """ Shuffle the tracks and ensure that the current track stays visible """ self.previousTracklist = [row[ROW_TRK] for row in self.list.getAllRows()] self.list.shuffle() if self.list.hasMark(): self.list.scroll_to_cell(self.list.getMark()) def showPopupMenu(self, list, path, button, time): """ The index parameter may be None """ popup = gtk.Menu() # Crop # crop = gtk.ImageMenuItem(_('Crop')) # crop.set_image(gtk.image_new_from_stock(gtk.STOCK_CUT, gtk.ICON_SIZE_MENU)) # crop.set_sensitive(path is not None) # crop.connect('activate', lambda item: self.tiraLed()) # popup.append(crop) # Remove # remove = gtk.ImageMenuItem(gtk.STOCK_REMOVE) # remove.set_sensitive(path is not None) # remove.connect('activate', lambda item: self.tiraLed()) # popup.append(remove) # popup.append(gtk.SeparatorMenuItem()) # Shuffle shuffle = gtk.ImageMenuItem(_('Shuffle Playlist')) shuffle.set_sensitive(len(list) != 0) shuffle.set_image(gtk.image_new_from_icon_name('stock_shuffle', gtk.ICON_SIZE_MENU)) shuffle.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE)) popup.append(shuffle) # Revert revert = gtk.ImageMenuItem(_('Revert Playlist')) revert.set_image(gtk.image_new_from_stock(gtk.STOCK_REVERT_TO_SAVED, gtk.ICON_SIZE_MENU)) revert.set_sensitive(self.previousTracklist is not None) revert.connect('activate', lambda item: self.revertTracklist()) popup.append(revert) # Clear clear = gtk.ImageMenuItem(_('Clear Playlist')) clear.set_sensitive(len(list) != 0) clear.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)) clear.connect('activate', lambda item: self.tiraLed()) popup.append(clear) popup.append(gtk.SeparatorMenuItem()) # Repeat repeat = gtk.CheckMenuItem(_('Repeat')) repeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS)) repeat.connect('toggled', lambda item: self.btnRepeat.clicked()) popup.append(repeat) popup.append(gtk.SeparatorMenuItem()) # Save save = gtk.ImageMenuItem(_('Save Playlist As...')) save.set_sensitive(len(list) != 0) save.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU)) save.connect('activate', lambda item: self.savePlaylist()) popup.append(save) popup.show_all() popup.popup(None, None, None, button, time) # --== Message handler ==-- def handleMsg(self, msg, params): """ A message has been received """ if msg == consts.MSG_EVT_PAUSED: self.onPausedToggled(consts.icoPause) elif msg == consts.MSG_EVT_STOPPED: self.onStopped() elif msg == consts.MSG_EVT_UNPAUSED: self.onPausedToggled(consts.icoPlay) elif msg == consts.MSG_EVT_APP_STARTED: self.onAppStarted() elif msg == consts.MSG_EVT_TRACK_ENDED_OK: self.onTrackEnded(False) elif msg == consts.MSG_EVT_TRACK_ENDED_ERROR: self.onTrackEnded(True) elif msg == consts.MSG_EVT_NEED_BUFFER: self.onBufferingNeeded() elif msg == consts.MSG_CMD_TRACKLIST_CLR: self.set(None, False) # elif msg == consts.MSG_CMD_TRACKLIST_SET: self.tiraLed() elif msg == consts.MSG_CMD_TRACKLIST_ADD: self.insert(params['tracks']) elif msg == consts.MSG_CMD_TRACKLIST_SHUFFLE: self.shuffleTracklist() elif msg == consts.MSG_CMD_TOGGLE_PAUSE and not self.list.hasMark(): self.jumpTo(0) elif msg == consts.MSG_CMD_PREVIOUS: self.jumpToPrevious() elif msg == consts.MSG_CMD_NEXT: self.jumpToNext() # --== GTK handlers ==-- def onButtonRepeat(self, btn): """ The 'repeat' button has been pressed """ tools.prefs.set(__name__, 'repeat-status', self.btnRepeat.get_active()) if self.list.hasMark(): modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__getPreviousTrackIdx() != -1, 'hasNext': self.__getNextTrackIdx() != -1}) def onButtonPressed(self, list, event, path): """ Play the selected track on double click, or show a popup menu on right click """ if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS and path is not None: # self.jumpTo(path[0]) self.tiraLed() elif event.button == 3: self.showPopupMenu(list, path, event.button, event.time) def onKeyboard(self, list, event): """ Remove the selection if possible """ if gtk.gdk.keyval_name(event.keyval) == 'D': if event.state & gtk.gdk.CONTROL_MASK: if event.state & gtk.gdk.SHIFT_MASK: self.removeSelection() elif gtk.gdk.keyval_name(event.keyval) == "Delete": self.tiraLed() elif gtk.gdk.keyval_name(event.keyval) == "R": if event.state & gtk.gdk.CONTROL_MASK: if event.state & gtk.gdk.SHIFT_MASK: self.jumpToNext() elif gtk.gdk.keyval_name(event.keyval) == "Z": if event.state & gtk.gdk.CONTROL_MASK: if event.state & gtk.gdk.SHIFT_MASK: self.onStopped() def onListModified(self, list): """ Some rows have been added/removed/moved """ # self.btnClear.set_sensitive(len(list) != 0) self.btnShuffle.set_sensitive(len(list) != 0) # Update playlist length and playlist position for all tracks for position, row in enumerate(self.list.getAllRows()): row[ROW_TRK].setPlaylistPos(position + 1) row[ROW_TRK].setPlaylistLen(len(self.list)) modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': self.getAllTracks(), 'playtime': self.playtime}) modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__getPreviousTrackIdx() != -1, 'hasNext': self.__getNextTrackIdx() != -1}) def onColumnVisibilityChanged(self, list, colTitle, visible): """ A column has been shown/hidden """ if colTitle == '#': colId = COL_TRCK_NUM elif colTitle == _('Title'): colId = COL_TITLE elif colTitle == _('Artist'): colId = COL_ARTIST elif colTitle == _('Album'): colId = COL_ALBUM elif colTitle == _('Length'): colId = COL_LENGTH elif colTitle == _('Genre'): colId = COL_GENRE elif colTitle == _('Date'): colId = COL_DATE else: colId = COL_PATH visibility = tools.prefs.get(__name__, 'columns-visibility-2', PREFS_DEFAULT_COLUMNS_VISIBILITY) visibility[colId] = visible tools.prefs.set(__name__, 'columns-visibility-2', visibility) def onDND(self, list, context, x, y, dragData, dndId, time): """ External Drag'n'Drop """ if dragData.data == '': context.finish(False, False, time) return dropInfo = list.get_dest_row_at_pos(x, y) # A list of filenames, without 'file://' at the beginning if dndId == consts.DND_DAP_URI: tracks = media.getTracks([urllib.url2pathname(uri) for uri in dragData.data.split()]) # A list of filenames starting with 'file://' elif dndId == consts.DND_URI: tracks = media.getTracks([urllib.url2pathname(uri)[7:] for uri in dragData.data.split()]) # A list of tracks elif dndId == consts.DND_DAP_TRACKS: tracks = [media.track.unserialize(serialTrack) for serialTrack in dragData.data.split('\n')] if dropInfo is None: self.insert(tracks) else: self.insert(tracks, dropInfo[0][0]) context.finish(True, False, time)
class Tracklist(modules.Module): """ This module manages the tracklist """ def __init__(self): """ Constructor """ handlers = { consts.MSG_CMD_NEXT: self.jumpToNext, consts.MSG_EVT_PAUSED: lambda: self.onPausedToggled(icons.pauseMenuIcon()), consts.MSG_EVT_STOPPED: self.onStopped, consts.MSG_EVT_UNPAUSED: lambda: self.onPausedToggled(icons.playMenuIcon()), consts.MSG_CMD_PREVIOUS: self.jumpToPrevious, consts.MSG_EVT_NEED_BUFFER: self.onBufferingNeeded, consts.MSG_EVT_APP_STARTED: self.onAppStarted, consts.MSG_CMD_TOGGLE_PAUSE: self.togglePause, consts.MSG_CMD_TRACKLIST_DEL: self.remove, consts.MSG_CMD_TRACKLIST_ADD: self.insert, consts.MSG_CMD_TRACKLIST_SET: self.set, consts.MSG_CMD_TRACKLIST_CLR: lambda: self.set(None, False, False), consts.MSG_CMD_TRACKLIST_PLAY: self.onTracklistPlay, consts.MSG_EVT_TRACK_ENDED_OK: lambda: self.onTrackEnded(False), consts.MSG_CMD_TRACKLIST_REPEAT: self.setRepeat, consts.MSG_EVT_TRACK_ENDED_ERROR: lambda: self.onTrackEnded(True), consts.MSG_CMD_TRACKLIST_SHUFFLE: self.shuffleTracklist, consts.MSG_CMD_TRACKLIST_PLAY_PAUSE: self.onTracklistPlayPause, } modules.Module.__init__(self, handlers) def __fmtColumnColor(self, col, cll, mdl, it): """ When playing, tracks already played are slightly greyed out """ style = self.window.get_style() played = self.list.hasMark() and mdl.get_path(it)[0] < self.list.getMark() if played: cll.set_property('foreground-gdk', style.text[gtk.STATE_INSENSITIVE]) else: cll.set_property('foreground-gdk', style.text[gtk.STATE_NORMAL]) def __fmtLengthColumn(self, col, cll, mdl, it): """ Format the column showing the length of the track (e.g., show 1:23 instead of 83) """ cll.set_property('text', tools.sec2str(mdl.get_value(it, ROW_LEN))) self.__fmtColumnColor(col, cll, mdl, it) def __getNextTrackIdx(self): """ Return the index of the next track, or -1 if there is none """ if self.list.hasMark(): if self.list.getMark() < (len(self.list) - 1): return self.list.getMark() + 1 elif self.btnRepeat.get_active(): return 0 return -1 def __hasNextTrack(self): """ Return whether there is a next track """ return self.__getNextTrackIdx() != -1 def __getPreviousTrackIdx(self): """ Return the index of the previous track, or -1 if there is none """ if self.list.hasMark(): if self.list.getMark() > 0: return self.list.getMark() - 1 elif self.btnRepeat.get_active(): return len(self.list) - 1 return -1 def __hasPreviousTrack(self): """ Return whether there is a previous track """ return self.__getPreviousTrackIdx() != -1 def jumpToNext(self): """ Jump to the next track, if any """ where = self.__getNextTrackIdx() if where != -1: self.jumpTo(where) def jumpToPrevious(self): """ Jump to the previous track, if any """ where = self.__getPreviousTrackIdx() if where != -1: self.jumpTo(where) def jumpTo(self, trackIdx, sendPlayMsg = True, forced = True): """ Jump to the track located at the given index """ if self.list.hasMark() and self.list.getItem(self.list.getMark(), ROW_ICO) != icons.errorMenuIcon(): self.list.setItem(self.list.getMark(), ROW_ICO, icons.nullMenuIcon()) self.list.setMark(trackIdx) self.list.scroll_to_cell(trackIdx) self.list.setItem(trackIdx, ROW_ICO, icons.playMenuIcon()) if sendPlayMsg: modules.postMsg(consts.MSG_CMD_PLAY, {'uri': self.list.getItem(trackIdx, ROW_TRK).getURI(), 'forced': forced}) modules.postMsg(consts.MSG_EVT_NEW_TRACK, {'track': self.list.getRow(trackIdx)[ROW_TRK]}) modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()}) def insert(self, tracks, playNow, position=None): """ Insert some tracks in the tracklist, append them if position is None """ rows = [[icons.nullMenuIcon(), track.getNumber(), track.getTitleOrFilename(), track.getArtist(), track.getExtendedAlbum(), track.getLength(), track.getBitrate(), track.getGenre(), track.getDate(), track.getFilename(), track.getURI(), track] for track in tracks] if len(rows) != 0: self.previousTracklist = [row[ROW_TRK] for row in self.list] for row in rows: self.playtime += row[ROW_LEN] self.list.insertRows(rows, position) if playNow: if position is not None: self.jumpTo(position) else: self.jumpTo(len(self.previousTracklist)) def set(self, tracks, playNow, keepCurrTrack = False): """ Replace the tracklist, clear it if tracks is None """ self.playtime = 0 # Save playlist only locally to this function # The insert() function would overwrite it otherwise previousTracklist = [row[ROW_TRK] for row in self.list] # Should we stop playback? sendStop = False keepTrackNewIdx = -1 # If we should keep the current track but it doesn't belong to the new playlist if keepCurrTrack and self.list.hasMark(): sendStop = True currTrack = self.list.getRow(self.list.getMark())[ROW_TRK] for idx, track in enumerate(tracks): if track == currTrack: sendStop = False keepTrackNewIdx = idx break # Or if we shouldn't start playback now or the new playlist is empty elif self.list.hasMark() and ((not playNow) or (tracks is None) or (len(tracks) == 0)): sendStop = True if sendStop: modules.postMsg(consts.MSG_CMD_STOP) self.list.clear() if tracks is not None and len(tracks) != 0: self.insert(tracks, playNow) self.previousTracklist = previousTracklist # Mark the current track if we kept the same one if keepCurrTrack and keepTrackNewIdx != -1: self.jumpTo(keepTrackNewIdx, False, False) def savePlaylist(self): """ Save the current tracklist to a playlist """ outFile = fileChooser.save(self.window, _('Save playlist'), 'playlist.m3u') if outFile is not None: allFiles = [row[ROW_TRK].getFilePath() for row in self.list.iterAllRows()] media.playlist.save(allFiles, outFile) def remove(self, idx=None): """ Remove the given track, or the selection if idx is None """ if idx is not None and (idx < 0 or idx >= len(self.list)): return hadMark = self.list.hasMark() self.previousTracklist = [row[ROW_TRK] for row in self.list] if idx is not None: self.playtime -= self.list.getRow(idx)[ROW_LEN] self.list.removeRow((idx, )) else: self.playtime -= sum([row[ROW_LEN] for row in self.list.iterSelectedRows()]) self.list.removeSelectedRows() self.list.unselectAll() if hadMark and not self.list.hasMark(): modules.postMsg(consts.MSG_CMD_STOP) def crop(self): """ Remove the unselected tracks """ hadMark = self.list.hasMark() self.previousTracklist = [row[ROW_TRK] for row in self.list] self.playtime = sum([row[ROW_LEN] for row in self.list.iterSelectedRows()]) self.list.cropSelectedRows() if hadMark and not self.list.hasMark(): modules.postMsg(consts.MSG_CMD_STOP) def revertTracklist(self): """ Back to the previous tracklist """ self.set(self.previousTracklist, False, True) self.previousTracklist = None def shuffleTracklist(self): """ Shuffle the tracks and ensure that the current track stays visible """ self.previousTracklist = [row[ROW_TRK] for row in self.list] self.list.shuffle() if self.list.hasMark(): self.list.scroll_to_cell(self.list.getMark()) def setRepeat(self, repeat): """ Set/Unset the repeat function """ if self.btnRepeat.get_active() != repeat: self.btnRepeat.clicked() def showPopupMenu(self, list, path, button, time): """ The index parameter may be None """ popup = gtk.Menu() # Crop crop = gtk.ImageMenuItem(_('Crop')) crop.set_image(gtk.image_new_from_stock(gtk.STOCK_CUT, gtk.ICON_SIZE_MENU)) popup.append(crop) if path is None: crop.set_sensitive(False) else: crop.connect('activate', lambda item: self.crop()) # Remove remove = gtk.ImageMenuItem(gtk.STOCK_REMOVE) popup.append(remove) if path is None: remove.set_sensitive(False) else: remove.connect('activate', lambda item: self.remove()) popup.append(gtk.SeparatorMenuItem()) # Shuffle shuffle = gtk.ImageMenuItem(_('Shuffle Playlist')) shuffle.set_image(gtk.image_new_from_icon_name('stock_shuffle', gtk.ICON_SIZE_MENU)) popup.append(shuffle) if len(list) == 0: shuffle.set_sensitive(False) else: shuffle.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE)) # Revert revert = gtk.ImageMenuItem(_('Revert Playlist')) revert.set_image(gtk.image_new_from_stock(gtk.STOCK_REVERT_TO_SAVED, gtk.ICON_SIZE_MENU)) popup.append(revert) if self.previousTracklist is None: revert.set_sensitive(False) else: revert.connect('activate', lambda item: self.revertTracklist()) # Clear clear = gtk.ImageMenuItem(_('Clear Playlist')) clear.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)) popup.append(clear) if len(list) == 0: clear.set_sensitive(False) else: clear.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR)) popup.append(gtk.SeparatorMenuItem()) # Repeat repeat = gtk.CheckMenuItem(_('Repeat')) repeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS)) repeat.connect('toggled', lambda item: self.btnRepeat.clicked()) popup.append(repeat) popup.append(gtk.SeparatorMenuItem()) # Save save = gtk.ImageMenuItem(_('Save Playlist As...')) save.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU)) popup.append(save) if len(list) == 0: save.set_sensitive(False) else: save.connect('activate', lambda item: self.savePlaylist()) popup.show_all() popup.popup(None, None, None, button, time) def togglePause(self): """ Start playing if not already playing """ if len(self.list) != 0 and not self.list.hasMark(): if self.list.getSelectedRowsCount() != 0: self.jumpTo(self.list.getFirstSelectedRowIndex()) else: self.jumpTo(0) # --== Message handlers ==-- def onAppStarted(self): """ This is the real initialization function, called when the module has been loaded """ wTree = tools.prefs.getWidgetsTree() self.playtime = 0 self.bufferedTrack = None self.previousTracklist = None # Retrieve widgets self.window = wTree.get_object('win-main') self.btnClear = wTree.get_object('btn-tracklistClear') self.btnRepeat = wTree.get_object('btn-tracklistRepeat') self.btnShuffle = wTree.get_object('btn-tracklistShuffle') self.btnClear.set_sensitive(False) self.btnShuffle.set_sensitive(False) # Create the list and its columns txtLRdr = gtk.CellRendererText() txtRRdr = gtk.CellRendererText() pixbufRdr = gtk.CellRendererPixbuf() txtRRdr.set_property('xalign', 1.0) # 'columns-visibility' may be broken, we should not use it (#311293) visible = tools.prefs.get(__name__, 'columns-visibility-2', PREFS_DEFAULT_COLUMNS_VISIBILITY) for (key, value) in PREFS_DEFAULT_COLUMNS_VISIBILITY.iteritems(): if key not in visible: visible[key] = value columns = (('#', [(pixbufRdr, gtk.gdk.Pixbuf), (txtRRdr, TYPE_INT)], (ROW_NUM, ROW_TIT), False, visible[COL_TRCK_NUM]), (_('Title'), [(txtLRdr, TYPE_STRING)], (ROW_TIT,), True, visible[COL_TITLE]), (_('Artist'), [(txtLRdr, TYPE_STRING)], (ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ARTIST]), (_('Album'), [(txtLRdr, TYPE_STRING)], (ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ALBUM]), (_('Length'), [(txtRRdr, TYPE_INT)], (ROW_LEN,), False, visible[COL_LENGTH]), (_('Bit Rate'), [(txtRRdr, TYPE_STRING)], (ROW_BTR, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_BITRATE]), (_('Genre'), [(txtLRdr, TYPE_STRING)], (ROW_GNR, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_GENRE]), (_('Date'), [(txtLRdr, TYPE_INT)], (ROW_DAT, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_DATE]), (_('Filename'), [(txtLRdr, TYPE_STRING)], (ROW_FIL,), False, visible[COL_FILENAME]), (_('Path'), [(txtLRdr, TYPE_STRING)], (ROW_PTH,), False, visible[COL_PATH]), (None, [(None, TYPE_PYOBJECT)], (None,), False, False)) self.list = ExtListView(columns, sortable=True, dndTargets=consts.DND_TARGETS.values(), useMarkup=False, canShowHideColumns=True) self.list.get_column(1).set_cell_data_func(txtLRdr, self.__fmtColumnColor) self.list.get_column(4).set_cell_data_func(txtRRdr, self.__fmtLengthColumn) self.list.enableDNDReordering() wTree.get_object('scrolled-tracklist').add(self.list) # GTK handlers self.list.connect('extlistview-dnd', self.onDND) self.list.connect('key-press-event', self.onKeyboard) self.list.connect('extlistview-modified', self.onListModified) self.list.connect('extlistview-button-pressed', self.onButtonPressed) self.list.connect('extlistview-selection-changed', self.onSelectionChanged) self.list.connect('extlistview-column-visibility-changed', self.onColumnVisibilityChanged) self.btnClear.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR)) self.btnRepeat.connect('toggled', self.onButtonRepeat) self.btnShuffle.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE)) # Restore preferences self.btnRepeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS)) # Set icons wTree.get_object('img-repeat').set_from_icon_name('stock_repeat', gtk.ICON_SIZE_BUTTON) wTree.get_object('img-shuffle').set_from_icon_name('stock_shuffle', gtk.ICON_SIZE_BUTTON) def onTrackEnded(self, withError): """ The current track has ended, jump to the next one if any """ currIdx = self.list.getMark() # If an error occurred with the current track, flag it as such if withError: self.list.setItem(currIdx, ROW_ICO, icons.errorMenuIcon()) # Find the next 'playable' track (not already flagged) if self.btnRepeat.get_active(): nbTracks = len(self.list) else: nbTracks = len(self.list) - 1 - currIdx for i in xrange(nbTracks): currIdx = (currIdx + 1) % len(self.list) if self.list.getItem(currIdx, ROW_ICO) != icons.errorMenuIcon(): track = self.list.getItem(currIdx, ROW_TRK).getURI() self.jumpTo(currIdx, track != self.bufferedTrack, forced = False) self.bufferedTrack = None return self.bufferedTrack = None modules.postMsg(consts.MSG_CMD_STOP) def onBufferingNeeded(self): """ The current track is close to its end, so we try to buffer the next one to avoid gaps """ where = self.__getNextTrackIdx() if where != -1: self.bufferedTrack = self.list.getItem(where, ROW_TRK).getURI() modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.bufferedTrack}) def onStopped(self): """ Playback has been stopped """ if self.list.hasMark(): currTrack = self.list.getMark() if self.list.getItem(currTrack, ROW_ICO) != icons.errorMenuIcon(): self.list.setItem(currTrack, ROW_ICO, icons.nullMenuIcon()) self.list.clearMark() def onPausedToggled(self, icon): """ Switch between paused and unpaused """ if self.list.hasMark(): self.list.setItem(self.list.getMark(), ROW_ICO, icon) def onTracklistPlay(self, idx, seconds): """ Play the given track """ self.jumpTo(idx) if seconds != 0: modules.postMsg(consts.MSG_CMD_SEEK, {'seconds': seconds}) def onTracklistPlayPause(self, idx, seconds): """ Play the given track but pause immediately """ self.onTracklistPlay(idx, seconds) modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE) # --== GTK handlers ==-- def onButtonRepeat(self, btn): """ The 'repeat' button has been pressed """ tools.prefs.set(__name__, 'repeat-status', self.btnRepeat.get_active()) modules.postMsg(consts.MSG_EVT_REPEAT_CHANGED, {'repeat': self.btnRepeat.get_active()}) if self.list.hasMark(): modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()}) def onButtonPressed(self, list, event, path): """ Play the selected track on double click, or show a popup menu on right click """ if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS and path is not None: self.jumpTo(path[0]) elif event.button == 3: self.showPopupMenu(list, path, event.button, event.time) def onKeyboard(self, list, event): """ Keyboard shortcuts """ keyname = gtk.gdk.keyval_name(event.keyval) if keyname == 'Delete': self.remove() elif keyname == 'Return': self.jumpTo(self.list.getFirstSelectedRowIndex()) elif keyname == 'space': modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE) elif keyname == 'Escape': modules.postMsg(consts.MSG_CMD_STOP) elif keyname == 'Left': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': -5}) elif keyname == 'Right': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': 5}) def onListModified(self, list): """ Some rows have been added/removed/moved """ self.btnClear.set_sensitive(len(list) != 0) self.btnShuffle.set_sensitive(len(list) != 0) # Update playlist length and playlist position for all tracks for position, row in enumerate(self.list): row[ROW_TRK].setPlaylistPos(position + 1) row[ROW_TRK].setPlaylistLen(len(self.list)) allTracks = [row[ROW_TRK] for row in self.list] modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': allTracks, 'playtime': self.playtime}) if self.list.hasMark(): modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()}) def onSelectionChanged(self, list, selectedRows): """ The selection has changed """ modules.postMsg(consts.MSG_EVT_TRACKLIST_NEW_SEL, {'tracks': [row[ROW_TRK] for row in selectedRows]}) def onColumnVisibilityChanged(self, list, colTitle, visible): """ A column has been shown/hidden """ if colTitle == '#': colId = COL_TRCK_NUM elif colTitle == _('Title'): colId = COL_TITLE elif colTitle == _('Artist'): colId = COL_ARTIST elif colTitle == _('Album'): colId = COL_ALBUM elif colTitle == _('Length'): colId = COL_LENGTH elif colTitle == _('Genre'): colId = COL_GENRE elif colTitle == _('Date'): colId = COL_DATE elif colTitle == _('Bit Rate'): colId = COL_BITRATE elif colTitle == _('File'): colId = COL_FILENAME else: colId = COL_PATH visibility = tools.prefs.get(__name__, 'columns-visibility-2', PREFS_DEFAULT_COLUMNS_VISIBILITY) visibility[colId] = visible tools.prefs.set(__name__, 'columns-visibility-2', visibility) def onDND(self, list, context, x, y, dragData, dndId, time): """ External Drag'n'Drop """ import urllib if dragData.data == '': context.finish(False, False, time) return # A list of filenames, without 'file://' at the beginning if dndId == consts.DND_DAP_URI: tracks = media.getTracks([urllib.url2pathname(uri) for uri in dragData.data.split()]) # A list of filenames starting with 'file://' elif dndId == consts.DND_URI: tracks = media.getTracks([urllib.url2pathname(uri)[7:] for uri in dragData.data.split()]) # A list of tracks elif dndId == consts.DND_DAP_TRACKS: tracks = [media.track.unserialize(serialTrack) for serialTrack in dragData.data.split('\n')] dropInfo = list.get_dest_row_at_pos(x, y) # Insert the tracks, but beware of the AFTER/BEFORE mechanism used by GTK if dropInfo is None: self.insert(tracks, False) elif dropInfo[1] == gtk.TREE_VIEW_DROP_AFTER: self.insert(tracks, False, dropInfo[0][0] + 1) else: self.insert(tracks, False, dropInfo[0][0]) context.finish(True, False, time)