from clMonty import monty from clLibrary import Library from PyQt4 import QtGui, QtCore from traceback import * import time import sys from misc import * from clSettings import settings from winConnect import winConnect from winSettings import winSettings clrRowSel=QtGui.QColor(180,180,180) class SongList(QtGui.QWidget): # CONFIGURATION VARIABLES # height of line in pxl lineHeight=20 margin=4 scrollbarWidth=15 minHeaderWidth=50 # alternating colors colors=[QtGui.QColor(255,255,255), QtGui.QColor(230,230,255)] clrSel=QtGui.QColor(100,100,180) # color of selection clrBg=QtGui.QColor(200,200,230) # background color onDoubleClick=None # array( (header, width, visible)+ ) headers=None songs=None # original songs numSongs=None # number of songs fSongs=None # filtered songs fNumSongs=None # number of filtered songs vScrollbar=None hScrollbar=None topRow=-1 numRows=-1 selRange=None # array of ranges of rows currently selected; relative to 0 selMode=False # currently in select mode? resizeCol=None # resizing a column? clrID=None # do we have to color a row with certain ID? [ID, color] scrollMult=1 # how many rows do we jump when scrolling by dragging xOffset=0 # offset for drawing. Is changed by hScrollbar resizeColumn=None # indicates this column should be recalculated redrawID=None # redraw this ID/row only def __init__(self, parent, headers, onDoubleClick): QtGui.QWidget.__init__(self, parent) self.onDoubleClick=onDoubleClick # we receive an array of strings; we convert that to an array of (header, width) self.headers=map(lambda h: [h, 250, True], headers) self.headers.insert(0, ['id', 30, True]) self.songs=None self.numSongs=None self.fSongs=None self.fNumSongs=None self.xOffset=0 self.resizeColumn=None self.vScrollbar=QtGui.QScrollBar(QtCore.Qt.Vertical, self) self.vScrollbar.setMinimum(0) self.vScrollbar.setMaximum(1) self.vScrollbar.setValue(0) self.hScrollbar=QtGui.QScrollBar(QtCore.Qt.Horizontal, self) self.hScrollbar.setMinimum(0) self.hScrollbar.setMaximum(1) self.hScrollbar.setValue(0) self.hScrollbar.setPageStep(200) self.topRow=0 self.numRows=0 self.selRows=[] self.selMode=False self.clrID=[-1,0] self.updateSongs([]) doEvents() self.connect(self.vScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onVScroll) self.connect(self.hScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onHScroll) self.setMouseTracking(True) self.setFocusPolicy(QtCore.Qt.TabFocus or QtCore.Qt.ClickFocus or QtCore.Qt.StrongFocus or QtCore.Qt.WheelFocus) self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) def sizeHint(self): return QtCore.QSize(10000,10000) def updateSongs(self, songs): self.songs=songs self.numSongs=len(songs) self.fSongs=songs self.fNumSongs=self.numSongs self.selRows=[] self.selMode=False self.resizeEvent(None) self.update() def selectedSongs(self): ret=[] for range in self.selRows: songs=filter(lambda song: song._data['id']>=range[0] and song._data['id']<=range[1], self.songs) for song in songs: ret.append(song) return ret def filter(self, strFilter): self.fSongs=filter(lambda song: strFilter in str(song).lower(), self.songs) self.fNumSongs=len(self.fSongs) self.resizeEvent(None) self.update() def colorID(self, id, clr): self.clrID=[id, clr] self.redrawID=id self.update() def selectRow(self, row): self.selRows=[[row,row]] self.update() def showColumn(self, column, show=True): self.headers[column][2]=show self.update() def autoSizeColumn(self, column): self.resizeColumn=column self.update() def visibleSongs(self): ret=[] for row in xrange(self.topRow, min(self.numSongs, self.topRow+self.numRows)-1): ret.append(self.fSongs[row]) return ret def ensureVisible(self, id): if len(filter(lambda song: song.getID()==id, self.visibleSongs())): return row=0 for song in self.fSongs: if song.getID()==id: self.vScrollbar.setValue(row-self.numRows/2) self.update() break row+=1 def onVScroll(self, value): " 'if value<0' needed because minimum can be after init <0 at some point ..." if value<0: value=0 if value>self.fNumSongs: value=self.fNumSongs self.topRow=value self.update() def onHScroll(self, value): self.xOffset=-self.hScrollbar.value()*2 self.update() def _pos2row(self, pos): return int(pos.y()/self.lineHeight)-1 def focusOutEvent(self, event): self.update() def focusInEvent(self, event): self.update() def wheelEvent(self, event): if self.vScrollbar.isVisible(): event.accept() numDegrees=event.delta() / 8 numSteps=5*numDegrees/15 self.vScrollbar.setValue(self.vScrollbar.value()-numSteps) def resizeEvent(self, event): self.numRows=int(self.height()/self.lineHeight)+1 # check vertical scrollbar if self.numRows+1>=self.fNumSongs: self.vScrollbar.setVisible(False) self.vScrollbar.setValue(0) else: self.vScrollbar.setVisible(True) self.vScrollbar.setPageStep(self.numRows-2) self.vScrollbar.setMinimum(0) self.vScrollbar.setMaximum(self.fNumSongs-self.numRows+1) self.vScrollbar.resize(self.scrollbarWidth, self.height()-self.lineHeight-1) self.vScrollbar.move(self.width()-self.vScrollbar.width()-1, self.lineHeight-1) # check horizontal scrollbar self.scrollWidth=0 for hdr in self.headers: if hdr[2]: self.scrollWidth+=hdr[1] if self.scrollWidth>self.width(): self.hScrollbar.setVisible(True) self.hScrollbar.setMinimum(0) self.hScrollbar.setMaximum((self.scrollWidth-self.width())/2) self.hScrollbar.resize(self.width(), self.lineHeight) self.hScrollbar.move(0, self.height()-self.lineHeight-1) self.vScrollbar.resize(self.vScrollbar.width(), self.vScrollbar.height()-self.lineHeight) self.vScrollbar.setMaximum(self.vScrollbar.maximum()+1) self.numRows-=1 else: self.hScrollbar.setVisible(False) self.hScrollbar.setValue(0) def mouseReleaseEvent(self, event): if self.selMode: self.selMode=False # find all consecutive ranges fSongs=self.fSongs self.selRows.sort() ranges=[] curRange=[] for song in fSongs[min(self.selRows[0]):max(self.selRows[0])+1]: id=song.getID() if len(curRange)==0 or curRange[-1]+1==id: curRange.append(id) else: ranges.append(curRange) curRange=[id] if len(curRange): ranges.append(curRange) # clean up ranges self.selRows=[] for range in ranges: self.selRows.append([range[0], range[-1]]) self.update() elif self.resizeCol!=None: self.resizeCol=None self.update() def mousePressEvent(self, event): self.setFocus() pos=event.pos() row=self._pos2row(pos) self.scrollMult=1 if row==-1: self.resizeCol=None x=0+self.xOffset i=0 for hdr in self.headers: if hdr[2]: x+=hdr[1] if abs(x-pos.x())<4: self.resizeCol=i i+=1 if self.resizeCol==None: self.selMode=True self.selRows=[[0, self.fNumSongs-1]] elif event.button()==QtCore.Qt.LeftButton: self.selRows=[[self.topRow+row,self.topRow+row]] self.selMode=True self.update() def mouseMoveEvent(self, event): pos=event.pos() row=self._pos2row(pos) if self.selMode: if row<0: row=0 if self.topRow>0: self.scrollMult+=0.1 jump=int(self.scrollMult)*int(abs(pos.y())/self.lineHeight) self.vScrollbar.setValue(self.vScrollbar.value()-jump) row=jump elif row>=self.numRows: self.scrollMult+=0.1 jump=int(self.scrollMult)*int(abs(self.height()-pos.y())/self.lineHeight) self.vScrollbar.setValue(self.vScrollbar.value()+jump) row=self.numRows-jump else: self.scrollMult=1 self.selRows[0][1]=row+self.topRow self.update() elif self.resizeCol!=None: prev=0 for i in xrange(self.resizeCol): hdr=self.headers[i] if hdr[2]: prev+=hdr[1] self.headers[self.resizeCol][1]=pos.x()-prev-self.xOffset if self.headers[self.resizeCol][1]=0: self.onDoubleClick() else: # auto-size column x=0+self.xOffset i=0 for hdr in self.headers: if hdr[2]: x+=hdr[1] if abs(x-pos.x())<4: self.autoSizeColumn(i) break i+=1 def _paintRow(self, p, row, y, width): song=self.fSongs[row] lineHeight=self.lineHeight margin=self.margin id=song._data['id'] # determine color of row. Default is row-color, but can be overridden by # (in this order): hover, selection, special row color! clr=self.colors[row%2] clrTxt=QtCore.Qt.black for range in self.selRows: if self.selMode==False and id>=range[0] and id<=range[1]: clr=self.clrSel clrTxt=QtCore.Qt.white elif self.selMode and row>=min(range) and row<=max(range): clr=self.clrSel clrTxt=QtCore.Qt.white if id==int(self.clrID[0]): clrTxt=QtCore.Qt.white clr=self.clrID[1] p.fillRect(QtCore.QRect(2, y, width-3, lineHeight), QtGui.QBrush(clr)) p.setPen(QtGui.QColor(230,230,255)) p.drawRect(QtCore.QRect(2, y, width-3, lineHeight)) p.setPen(QtCore.Qt.black) x=margin+self.xOffset for hdr in self.headers: if hdr[2]: rect=p.boundingRect(x, y, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, str(song.getTag(hdr[0]))) text=str(song.getTag(hdr[0])) p.setPen(clrTxt) p.drawText(x, y+1, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text) if rect.width()>hdr[1]-margin: p.fillRect(x+hdr[1]-15,y+1,15,lineHeight-1, QtGui.QBrush(clr)) p.drawText(x+hdr[1]-15,y+1,15,lineHeight-1, QtCore.Qt.AlignLeft, "...") x+=hdr[1] p.setPen(QtCore.Qt.black) p.drawLine(QtCore.QPoint(x-margin,y), QtCore.QPoint(x-margin,y+lineHeight)) def paintEvent(self, event): lineHeight=self.lineHeight margin=self.margin # OTHER VARS p=QtGui.QPainter(self) selRows=self.selRows width=self.width() if self.vScrollbar.isVisible(): width-=self.scrollbarWidth if self.resizeColumn!=None: hdr=self.headers[self.resizeColumn][0] w=self.minHeaderWidth for song in self.songs: rect=p.boundingRect(10,10,1,1, QtCore.Qt.AlignLeft, str(song.getTag(hdr))) w=max(rect.width(), w) self.headers[self.resizeColumn][1]=w+2*margin self.resizeColumn=None self.resizeEvent(None) if self.redrawID!=None: y=lineHeight for row in xrange(self.topRow, min(self.fNumSongs, self.topRow+self.numRows)): if self.fSongs[row]._data['id']==self.redrawID: self._paintRow(p, row, y, width) y+=lineHeight self.redrawID=None return # paint the headers! p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), QtGui.QBrush(QtCore.Qt.lightGray)) p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1)) x=margin+self.xOffset for hdr in self.headers: if hdr[2]: p.drawText(QtCore.QPoint(x, lineHeight-margin), hdr[0]) x+=hdr[1] p.drawLine(QtCore.QPoint(x-margin,0), QtCore.QPoint(x-margin,lineHeight)) if self.songs==None: return # fill the records! y=lineHeight for row in xrange(self.topRow, min(self.fNumSongs, self.topRow+self.numRows)): self._paintRow(p, row, y, width) y+=lineHeight if y4: self._rowColorAdder=-1*self._rowColorAdder def addLibrarySelToPlaylist(self): paths=[] songs=self.lstLibrary.selectedSongs() cnt=len(songs) steps=5*cnt/100+1 row=-1 for song in songs: row+=1 paths.append(song.getFilepath()) if row % steps==0: self.setStatus('Adding '+str(cnt)+' songs to library ...'+str(int(100*row/cnt))+'%') doEvents() monty.addToPlaylist(paths) self.setStatus('') doEvents() self.fillPlaylist() def onSysTrayClick(self, reason): if self.isVisible(): self.hide() else: self.show() def onPlaylistKeyPress(self, event): lst=self.lstPlaylist " remove selection from playlist using DELETE-key" if event.matches(QtGui.QKeySequence.Delete): ids=lst.selectedIds() self.setStatus('Deleting '+str(len(ids))+' songs from playlist ...') doEvents() monty.deleteFromPlaylist(ids) self.setStatus('') doEvents() self.fillPlaylist() return QtGui.QWidget.keyPressEvent(self, event) def onLibraryKeyPress(self, event): " add selection, or entire library to playlist using ENTER-key" if event.key()==QtCore.Qt.Key_Enter or event.key()==QtCore.Qt.Key_Return: self.addLibrarySelToPlaylist() return QtGui.QWidget.keyPressEvent(self, event) def enableObjects(self, objects, enable): for object in objects: object.setEnabled(enable) def onStateChange(self, params): self.updatePlayingInfo() newState=params['newState'] self.enableObjects([self.slrTime, self.btnStop, self.btnNext, self.btnPrevious], newState!='stop') if newState=='play': self.btnPlayPause.setText('pause') elif newState=='pause' or newState=='stop': self.btnPlayPause.setText('play') def onTimeChange(self, params): self.updatePlayingInfo() if self.slrTime.isSliderDown()==False: self.slrTime.setValue(params['newTime']) def onSongChange(self, params): lst=self.lstPlaylist lst.colorID(int(params['newSongID']), clrRowSel) if params['newSongID']==-1: self.slrTime.setEnabled(False) else: try: self.slrTime.setMaximum(monty.getStatus()['length']) self.slrTime.setEnabled(True) lst.ensureVisible(params['newSongID']) except: pass def onVolumeChange(self, params): self.slrVolume.setValue(params['newVolume']) def onReady(self, params): self.initialiseData() self._timerID=self.startTimer(200) def onConnect(self, params): self.setStatus('Restoring library ...') doEvents() def initialiseData(self): self.setStatus("Retrieving library ...") doEvents() self.mLibrary=Library() doEvents() self.setStatus("Filling library ...") doEvents() self.fillLibrary() doEvents() self.setStatus("Filling playlist ...") doEvents() self.fillPlaylist() doEvents() self.setStatus("Doing the rest ...") doEvents() self.enableObjects(self.disableObjects, True) self.updatePlayingInfo() self.setStatus("") doEvents() def onDisconnect(self, params): self.enableObjects(self.disableObjects, False) def fillPlaylist(self): self.lstPlaylist.update(monty.listPlaylist()) def fillLibrary(self): self.lstLibrary.update(self.mLibrary.list()) def onLibraryDoubleClick(self): self.addLibrarySelToPlaylist() def onPlaylistDoubleClick(self): monty.play(self.lstPlaylist.getSelItemID()) def onTimeSliderChange(self): monty.seek(self.slrTime.value()) def onVolumeSliderChange(self): monty.setVolume(self.slrVolume.value()) def onBtnPlayPauseClick(self): status=monty.getStatus() if status['state']=='play': monty.pause() elif status['state']=='stop': if self.lstPlaylist.getSelItemID()==-1: self.lstPlaylist.selectRow(0) self.onPlaylistDoubleClick() else: monty.resume() self.updatePlayingInfo() def onBtnStopClick(self): monty.stop() self.updatePlayingInfo() def onBtnPreviousClick(self): monty.previous() self.updatePlayingInfo() def onBtnNextClick(self): monty.next() self.updatePlayingInfo() def onBtnSettingsClick(self): self.winSettings=winSettings() self.winSettings.show()