Use QPalette instead of hardcoded colors.
[nephilim.git] / wgSongList.py
blob0088621140c9e31f22d8d6999ac452fbd9604312
1 from PyQt4 import QtGui, QtCore, QtSvg
2 from PyQt4.QtGui import QPalette
4 import sys
5 from traceback import print_exc
6 import time
8 from misc import *
9 from clSong import Song
10 from clSettings import settings
11 import format
13 # constants used for fSongs
14 LIB_ROW=0
15 LIB_VALUE=1
16 LIB_INDENT=2
17 LIB_NEXTROW=3
18 LIB_EXPANDED=4 # values: 0, 1 or 2 (==song)
19 LIB_PARENT=5
21 class DoUpdate(QtCore.QEvent):
22 def __init__(self):
23 QtCore.QEvent.__init__(self, QtCore.QEvent.User)
24 class DoResize(QtCore.QEvent):
25 def __init__(self):
26 QtCore.QEvent.__init__(self, QtCore.QEvent.User)
28 class SongList(QtGui.QWidget):
29 """The SongList widget is a list optimized for displaying an array of songs, with filtering option."""
30 # CONFIGURATION VARIABLES
31 " font size in pxl"
32 fontSize=12 #TODO:make this selectable
33 " height of line in pxl"
34 lineHeight = fontSize + 4
35 " margin"
36 margin=4
37 vmargin=(lineHeight-fontSize)/2-1
38 " width of the vscrollbar"
39 scrollbarWidth=15
40 " minimum column width"
41 minColumnWidth=50
42 " palette"
43 palette = QPalette()
44 " colors for alternating rows"
45 colors = [palette.color(QPalette.Base), palette.color(QPalette.AlternateBase)]
46 " color of selection"
47 clrSel = palette.color(QPalette.Highlight)
48 " background color"
49 clrBg = palette.color(QPalette.Window)
50 " indentation of hierarchy, in pixels"
51 indentation=lineHeight
53 " what function to call when the list is double clicked"
54 onDoubleClick=None
56 mode='playlist' # what mode is the songlist in? values: 'playlist', 'library'
57 " the headers: ( (header, width, visible)+ )"
58 headers=None
59 songs=None # original songs
60 numSongs=None # number of songs
62 # 'edited' songs
63 # in playlist mode, this can only filtering
64 # in library mode, this indicates all entries: (row, tag-value, indentation, next-row, expanded)*
65 fSongs=None # filtered songs
66 numVisEntries=None # number of entries that are visible (including when scrolling)
69 levels=[] # levels from the groupBy in library-mode
70 groupByStr='' # groupBy used in library-mode
72 vScrollbar=None
73 hScrollbar=None
75 topRow=-1
76 numRows=-1 # total number of rows that can be visible in 1 time
77 selRows=None # ranges of selected rows: ( (startROw,endRow)* )
78 selIDs=None # ranges of selected IDs: [ [startID,endID] ]
79 selMiscs=None # array of indexes for selected non-songs in library mode
81 selMode=False # currently in select mode?
82 resizeCol=None # resizing a column?
83 clrID=None # do we have to color a row with certain ID? [ID, color]
84 scrollMult=1 # how many rows do we jump when scrolling by dragging
85 xOffset=0 # offset for drawing. Is changed by hScrollbar
86 resizeColumn=None # indicates this column should be recalculated
87 redrawID=None # redraw this ID/row only
89 wgGfxAlbum=QtSvg.QSvgRenderer('gfx/gnome-cd.svg')
90 wgGfxArtist=QtSvg.QSvgRenderer('gfx/user_icon.svg')
93 def __init__(self, parent, name, headers, onDoubleClick):
94 QtGui.QWidget.__init__(self, parent)
95 self.onDoubleClick=onDoubleClick
97 # we receive an array of strings; we convert that to an array of (header, width)
98 self.name=name
99 # load the headers, and fetch from the settings the width and visibility
100 self.headers=map(lambda h: [h, int(settings.get('l%s.%s.width'%(self.name,h),250))
101 , settings.get('l%s.%s.visible'%(self.name,h),'1')=='1'], headers)
102 self.headers.insert(0, ['id', 30, settings.get('l%s.%s.visible'%(self.name,'id'),'0')=='1'])
103 self.songs=None
104 self.numSongs=None
105 self.fSongs=None
106 self.selMiscs=[]
107 self.numVisEntries=None
108 self.xOffset=0
109 self.resizeColumn=None
111 self._filters=[]
113 self.vScrollbar=QtGui.QScrollBar(QtCore.Qt.Vertical, self)
114 self.vScrollbar.setMinimum(0)
115 self.vScrollbar.setMaximum(1)
116 self.vScrollbar.setValue(0)
118 self.hScrollbar=QtGui.QScrollBar(QtCore.Qt.Horizontal, self)
119 self.hScrollbar.setMinimum(0)
120 self.hScrollbar.setMaximum(1)
121 self.hScrollbar.setValue(0)
122 self.hScrollbar.setPageStep(200)
124 self.topRow=0
125 self.numRows=0
126 self.selRows=[]
127 self.selMode=False
128 self.clrID=[-1,0]
130 self.updateSongs([])
131 doEvents()
133 self.connect(self.vScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onVScroll)
134 self.connect(self.hScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onHScroll)
136 self.setMouseTracking(True)
137 self.setFocusPolicy(QtCore.Qt.TabFocus or QtCore.Qt.ClickFocus
138 or QtCore.Qt.StrongFocus or QtCore.Qt.WheelFocus)
140 self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
141 font=QtGui.QFont()
142 font.setPixelSize(self.fontSize)
143 font.setFamily('Liberation Sans') #TODO make this selectable
144 self.setFont(font)
146 def sizeHint(self):
147 return QtCore.QSize(10000,10000)
149 def getSongs(self):
150 return self.songs
152 def customEvent(self, event):
153 if isinstance(event, DoResize):
154 self.resizeEvent(None)
155 self.update()
156 elif isinstance(event, DoUpdate):
157 self.update()
158 else:
159 Logger.extended("wgSongList::Unknown event "+str(event))
161 def setMode(self, mode, groupBy=''):
162 self.selRows=[]
163 self.selIDs=[]
164 self.selMode=False
166 if mode=='playlist':
167 self.fSongs=self.songs
168 self.numVisEntries=len(self.fSongs)
169 elif mode=='library':
170 self.groupBy(groupBy)
171 else:
172 raise Exception('Unknown mode %' %(mode))
174 self.mode=mode
175 QtCore.QCoreApplication.postEvent(self, DoResize())
177 def groupBy(self, groupBy, strFilter=''):
178 self.groupByStr=groupBy
179 self.levels=groupBy.split('/')
180 strFilter=strFilter.strip()
182 l=[]
183 formats=[]
184 for i in xrange(0,len(self.levels)):
185 formats.append(format.compile(self.levels[i]))
186 # TODO also take l[1] etc into account?
187 U="(Unknown)"
188 xtra={"album":U, "artist":U, "date":"", "genre":U}
189 compare=lambda left, right: cmp(\
190 formats[0](format.params(left)).lower(), \
191 formats[0](format.params(right)).lower() \
194 songs=self.songs
195 if strFilter!='':
196 songs=filter(lambda song: song.match(strFilter), songs)
197 songs=sorted(songs, compare)
199 numLevels=len(self.levels)
200 self.fSongs=[[0, 'dummy', 0, -1, False]]
201 row=0
202 # four levels ought to be enough for everyone
203 curLevels=[[None,0], [None,0], [None,0], [None,0]] # contains the values of current levels
204 curLevel=0 # current level we're in
205 parents=[-1,-1,-1,-1] # index of parent
206 for song in songs:
207 if not(self.songs):
208 return
209 for level in xrange(numLevels):
210 # does the file have the required tag?
211 if not formats[level](format.params(song, {}, xtra))==curLevels[level][LIB_ROW]:
212 finalRow=row
213 for i in xrange(level,numLevels):
214 tagValue2=formats[i](format.params(song, {}, xtra))
216 self.fSongs[curLevels[i][1]][LIB_NEXTROW]=finalRow
217 self.fSongs.append([row, tagValue2, i, row+1, 0, parents[i]])
218 parents[i+1]=row
220 row+=1
221 curLevels[i]=[tagValue2, row]
222 curLevel=numLevels
223 self.fSongs.append([row, song, curLevel, row+1, 2, parents[curLevel]])
224 row+=1
226 # update last entries' next-row of each level
227 # If we have e.g. artist/album, then the last artist and last album of that
228 # artist have to be repointed to the end of the list, else problems arise
229 # showing those entries ...
230 # indicate for each level whether we have processed that level yet
231 processed=[False, False, False, False, False]
232 numFSongs=len(self.fSongs)
233 for i in xrange(numFSongs-1,0,-1):
234 song=self.fSongs[i]
235 # look for last top-level entry
236 if song[LIB_INDENT]==0:
237 song[LIB_NEXTROW]=numFSongs
238 break
239 if processed[song[LIB_INDENT]]==False:
240 song[LIB_NEXTROW]=numFSongs
241 processed[song[LIB_INDENT]]=True
244 # remove the dummy
245 self.fSongs.pop(0)
247 self.numVisEntries=len(filter(lambda entry: entry[LIB_INDENT]==0, self.fSongs))
249 def updateSongs(self, songs):
250 """Update the displayed songs and clears selection."""
251 self.songs=songs
252 self.numSongs=len(songs)
254 self.setMode(self.mode, self.groupByStr)
256 self.redrawID=None
257 QtCore.QCoreApplication.postEvent(self, DoUpdate())
259 def selectedSongs(self):
260 """Returns the list of selected songs."""
261 ret=[]
262 if self.mode=='playlist':
263 cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1]
264 elif self.mode=='library':
265 cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1]
266 for range in self.selIDs:
267 # look for the songs in the current range
268 songs=filter(cmp, self.songs)
269 # add songs in range
270 ret.extend(songs)
271 return ret
273 def killFilters(self):
274 songs=self.songs
275 self.songs=None
276 while (len(self._filters)):
277 # wait 'till everything's cleared
278 doEvents()
279 self.songs=songs
282 # contains filters ready to be applied; only the top one will be used
283 _filters=[]
284 def filter(self, strFilter):
285 """Filter songs according to $strFilter."""
287 self._filters.append(strFilter)
288 strFilter=self._filters[-1]
290 try:
291 if self.mode=='playlist':
292 self.fSongs=filter(lambda song: song.match(strFilter), self.songs)
293 self.numVisEntries=len(self.fSongs)
294 else:
295 self.groupBy(self.groupByStr, strFilter)
296 except:
297 # we might get here because self.songs is None
298 pass
299 self._filters=[]
300 QtCore.QCoreApplication.postEvent(self, DoResize())
302 def colorID(self, id, clr):
303 """Color the row which contains song with id $id $clr."""
304 self.clrID=[id, clr]
305 self.redrawID=id
307 QtCore.QCoreApplication.postEvent(self, DoUpdate())
309 def selectRow(self, row):
310 """Make $row the current selection."""
311 self.selRows=[[row,row]]
313 QtCore.QCoreApplication.postEvent(self, DoUpdate())
315 def showColumn(self, column, show=True):
316 """Hide or show column $column."""
317 self.headers[column][2]=show
319 self.update()
321 def autoSizeColumn(self, column):
322 """Resizes column $column to fit the widest entry in the non-filtered songs."""
323 # we can't calculate it here, as retrieving the text-width can only
324 # be done in the paintEvent method ...
325 self.resizeColumn=column
327 QtCore.QCoreApplication.postEvent(self, DoUpdate())
329 def visibleSongs(self):
330 """Get the songs currently visible."""
331 ret=[]
332 if self.mode=='playlist':
333 for row in xrange(self.topRow, min(self.numSongs, self.topRow+self.numRows)-1):
334 ret.append(self.fSongs[row])
335 elif self.mode=='library':
336 # note that if everything is folded, there'll be no songs!
337 entries=self.fSongs
338 index=self.libFirstVisRowIndex()
339 count=0
340 while index>=0 and index<len(entries) and count<self.numRows:
341 entry=self.fSongs[index]
342 if isinstance(entry[LIB_VALUE], Song):
343 ret.append(entry[LIB_VALUE])
344 index=self.libIthVisRowIndex(index)
345 count+=1
346 return ret
348 def ensureVisible(self, id):
349 """Make sure the song with $id is visible."""
350 if len(filter(lambda song: song.getID()==id, self.visibleSongs())):
351 return
352 row=0
353 ok=False
354 if self.mode=='playlist':
355 # playlist mode is simple: just hop to the song with id!
356 for song in self.fSongs:
357 row+=1
358 if song.getID()==id:
359 ok=True
360 break
361 elif self.mode=='library':
362 # library mode is more complex: we must find out how many rows are visible,
363 # and expand the parents of the song, if necessary
364 indLevel=0 # indicates what is the deepest level that is expanded for the current entry
365 # thus if current indent<=indLevel, then it is visible
366 for entry in self.fSongs:
367 if entry[LIB_EXPANDED]==1:
368 indLevel=max(entry[LIB_INDENT]+1, indLevel)
369 elif entry[LIB_EXPANDED]==0:
370 indLevel=min(entry[LIB_INDENT], indLevel)
371 if entry[LIB_INDENT]<=indLevel:
372 row+=1
374 #print "%s -> %s"%(str(indLevel), str(entry))
375 if isinstance(entry[LIB_VALUE], Song) and entry[LIB_VALUE].getID()==id:
376 # expand parents
377 # must be expanded in reverse order, else we will count too many
378 # entries ...
379 parents=[]
380 while entry[LIB_PARENT]>=0:
381 entry=self.fSongs[entry[LIB_PARENT]]
382 parents.append(entry)
383 row-=1
384 parents.reverse()
385 for parent in parents:
386 self.libExpand(parent)
387 ok=True
388 break
390 if ok:
391 self.vScrollbar.setValue(row-self.numRows/2)
392 self.update()
395 def onVScroll(self, value):
396 # 'if value<0' needed because minimum can be after init <0 at some point ...
397 if value<0: value=0
398 if value>self.numVisEntries:value=self.numVisEntries
399 self.topRow=value
401 self.update()
403 def onHScroll(self, value):
404 self.xOffset=-self.hScrollbar.value()*2
405 self.update()
407 def _pos2row(self, pos):
408 return int(pos.y()/self.lineHeight)-1
409 def _row2entry(self, row):
410 entry=self.fSongs[0]
411 try:
412 while row>0:
413 if entry[LIB_EXPANDED]:
414 entry=self.fSongs[entry[LIB_ROW]+1]
415 else:
416 entry=self.fSongs[entry[LIB_NEXTROW]]
417 row-=1
418 except:
419 return None
420 return entry
422 def focusOutEvent(self, event):
423 self.update()
424 def focusInEvent(self, event):
425 self.update()
426 def wheelEvent(self, event):
427 if self.vScrollbar.isVisible():
428 event.accept()
429 numDegrees=event.delta() / 8
430 numSteps=5*numDegrees/15
431 self.vScrollbar.setValue(self.vScrollbar.value()-numSteps)
433 def resizeEvent(self, event):
434 # max nr of rows shown
435 self.numRows=int(self.height()/self.lineHeight)
437 # check vertical scrollbar
438 if self.numRows>self.numVisEntries:
439 self.vScrollbar.setVisible(False)
440 self.vScrollbar.setValue(0)
441 else:
442 self.vScrollbar.setVisible(True)
443 self.vScrollbar.setPageStep(self.numRows-2)
444 self.vScrollbar.setMinimum(0)
445 self.vScrollbar.setMaximum(self.numVisEntries-self.numRows+1)
446 self.vScrollbar.resize(self.scrollbarWidth, self.height()-self.lineHeight-1)
447 self.vScrollbar.move(self.width()-self.vScrollbar.width()-1, self.lineHeight-1)
449 # check horizontal scrollbar
450 self.scrollWidth=0
451 if self.mode=='playlist':
452 for hdr in self.headers:
453 if hdr[2]:
454 self.scrollWidth+=hdr[1]
456 if self.scrollWidth>self.width():
457 self.hScrollbar.setVisible(True)
458 self.hScrollbar.setMinimum(0)
459 self.hScrollbar.setMaximum((self.scrollWidth-self.width())/2)
460 self.hScrollbar.resize(self.width()-4, self.scrollbarWidth)
461 self.hScrollbar.move(2, self.height()-self.hScrollbar.height()-1)
463 # some changes because the hScrollbar takes some vertical space ...
464 self.vScrollbar.resize(self.vScrollbar.width(), self.vScrollbar.height()-self.lineHeight)
465 self.vScrollbar.setMaximum(self.vScrollbar.maximum()+1)
467 self.numRows-=1
468 else:
469 self.hScrollbar.setVisible(False)
470 self.hScrollbar.setValue(0)
472 def libExpand(self, entry):
473 if entry and entry[LIB_EXPANDED]==0:
474 self.libToggle(entry)
475 def libCollapse(self, entry):
476 if entry and entry[LIB_EXPANDED]==1:
477 self.libToggle(entry)
479 def libToggle(self, entry):
480 """Toggles expanded state. Returns new state"""
481 expanded=entry[LIB_EXPANDED]
482 if expanded!=2:
483 # there was a '+' or a '-'!
484 entry[LIB_EXPANDED]=(expanded+1)%2
485 ret=entry[LIB_EXPANDED]
486 # we must find out how many entries have appeared/disappeard
487 # while collapsing.
488 visibles=0 # how many new elements have appeared?
489 i=entry[LIB_ROW]+1 # current element looking at
490 indLevel=self.fSongs[i][LIB_INDENT]
491 while i<=entry[LIB_NEXTROW]-1 and i<len(self.fSongs):
492 entry2=self.fSongs[i]
493 if entry2[LIB_EXPANDED]==1:
494 indLevel=max(entry2[LIB_INDENT]+1, indLevel)
495 elif entry2[LIB_EXPANDED]==0:
496 indLevel=min(entry2[LIB_INDENT], indLevel)
497 if entry2[LIB_INDENT]<=indLevel:
498 visibles+=1
500 if entry2[LIB_EXPANDED]==0:
501 i=entry2[LIB_NEXTROW]
502 else:
503 i+=1
505 mult=1
506 if ret==0: mult=-1
507 self.numVisEntries+=mult*visibles
508 self.resizeEvent(None)
509 return ret
511 return 2
513 def mousePressEvent(self, event):
514 self.setFocus()
515 pos=event.pos()
516 row=self._pos2row(pos)
518 done=False # indicates whether some action has been done or not
519 if self.mode=='playlist':
520 self.scrollMult=1
521 if row==-1:
522 # we're clicking in the header!
523 self.resizeCol=None
524 x=0+self.xOffset
526 # check if we're clicking between two columns, if so: resize mode!
527 for hdr in self.headers:
528 if hdr[2]:
529 x+=hdr[1]
530 if abs(x-pos.x())<4:
531 self.resizeCol=i
532 done=True
533 i+=1
534 elif self.mode=='library':
535 entry=self._row2entry(row+self.topRow)
536 if not entry:
537 entry=self.fSongs[len(self.fSongs)-1]
538 if entry and pos.x()>(1+entry[LIB_INDENT])*self.indentation \
539 and pos.x()<(1+entry[LIB_INDENT]+3/2)*self.indentation:
540 # we clicked in the margin, to expand or collapse
541 self.libToggle(entry)
542 done=True
544 if done==False:
545 self.selMode=True
546 self.selIDs=[]
547 self.selMiscs=[]
548 if row==-1 and self.resizeCol==None:
549 # we're not resizing, thus we can select all!
550 self.selRows=[[0, len(self.fSongs)]]
551 elif row>=0:
552 # we start selection mode
553 if self.mode=='playlist':
554 self.selRows=[[self.topRow+row,self.topRow+row]]
555 elif self.mode=='library':
556 self.selRows=[[entry[LIB_ROW], entry[LIB_NEXTROW]-1]]
557 self.selMode=True
559 self.update()
561 def mouseMoveEvent(self, event):
562 pos=event.pos()
563 row=self._pos2row(pos)
564 if self.selMode:
565 # we're in selection mode
566 if row<0:
567 # scroll automatically when going out of the widget
568 row=0
569 if self.topRow>0:
570 self.scrollMult+=0.1
571 jump=int(self.scrollMult)*int(abs(pos.y())/self.lineHeight)
572 self.vScrollbar.setValue(self.vScrollbar.value()-jump)
573 row=jump
574 elif row>=self.numRows:
575 # scroll automatically when going out of the widget
576 self.scrollMult+=0.1
577 jump=int(self.scrollMult)*int(abs(self.height()-pos.y())/self.lineHeight)
578 self.vScrollbar.setValue(self.vScrollbar.value()+jump)
579 row=self.numRows-jump
580 else:
581 # reset the scrollMultiplier
582 self.scrollMult=1
584 if self.mode=='playlist':
585 self.selRows[0][1]=row+self.topRow
586 elif self.mode=='library':
587 self.selRows[0][1]=self.libIthVisRowIndex(self.libIthVisRowIndex(0,self.topRow), row)
588 self.update()
589 elif self.resizeCol!=None:
590 row-=1
591 # ohla, we're resizing a column!
592 prev=0
593 # calculate where we are
594 for i in xrange(self.resizeCol):
595 hdr=self.headers[i]
596 if hdr[2]:
597 prev+=hdr[1]
598 self.headers[self.resizeCol][1]=pos.x()-prev-self.xOffset
599 # minimum width check?
600 if self.headers[self.resizeCol][1]<self.minColumnWidth:
601 self.headers[self.resizeCol][1]=self.minColumnWidth
602 self.resizeEvent(None)
603 self.update()
605 def mouseReleaseEvent(self, event):
606 if self.selMode and len(self.selRows):
607 # we were selecting, but now we're done.
608 # We have to transform one range of rows
609 # into range of selected IDs
610 # The problem is that the list can be filtered, and that
611 # consequtive, visible rows aren't always directly
612 # consequtive in the unfiltered list.
613 self.selMode=False # exit selection mode
614 fSongs=self.fSongs
615 self.selMiscs=[]
616 ranges=[]
617 curRange=[]
618 # loop over all rows that are selected
619 for entry in fSongs[min(self.selRows[0]):max(self.selRows[0])+1]:
620 song=None
621 if isinstance(entry,Song):
622 song=entry
623 elif isinstance(entry[LIB_VALUE],Song):
624 song=entry[LIB_VALUE]
625 else:
626 self.selMiscs.append(entry[LIB_ROW])
628 if song!=None:
629 id=song.getID()
630 # is this song directly after the previous row?
631 if len(curRange)==0 or curRange[-1]+1==id:
632 curRange.append(id)
633 else:
634 ranges.append(curRange)
635 curRange=[id]
636 if len(curRange):
637 ranges.append(curRange)
638 # clean up ranges
639 self.selRows=[]
640 self.selIDs=[]
641 for range in ranges:
642 self.selIDs.append([range[0], range[-1]])
643 self.update()
645 elif self.resizeCol!=None:
646 # store this new width!
647 self.saveColumnWidth(self.resizeCol)
648 # we're not resizing anymore!
649 self.resizeCol=None
650 self.update()
651 def saveColumnWidth(self, col):
652 settings.set('l%s.%s.width'%(self.name,self.headers[col][0]), self.headers[col][1])
653 def mouseDoubleClickEvent(self, event):
654 pos=event.pos()
655 row=self._pos2row(pos)
656 if row>=0:
657 self.onDoubleClick()
658 else:
659 # auto-size column
660 x=0+self.xOffset
662 for hdr in self.headers:
663 if hdr[2]:
664 x+=hdr[1]
665 if abs(x-pos.x())<4:
666 self.autoSizeColumn(i)
667 break
668 i+=1
670 def _paintPlaylist(self, p):
671 self.redrawID=None
674 lineHeight=self.lineHeight
675 margin=self.margin
676 vmargin=self.vmargin
677 selRows=self.selRows
678 width=self.width()
679 if self.vScrollbar.isVisible():
680 width-=self.scrollbarWidth
682 if self.resizeColumn!=None:
683 # we're autoresizing!
684 # must be done here, because only here we can check the textwidth!
685 # This is because of limitations it can be only be done in paintEvent
686 hdr=self.headers[self.resizeColumn][0]
687 w=self.minColumnWidth
688 # loop over all visible songs ...
689 for song in self.fSongs:
690 rect=p.boundingRect(10,10,1,1, QtCore.Qt.AlignLeft, song.getTag(hdr))
691 w=max(rect.width(), w)
692 self.headers[self.resizeColumn][1]=w+2*margin
693 self.saveColumnWidth(self.resizeColumn)
694 self.resizeColumn=None
695 self.resizeEvent(None)
696 if self.redrawID!=None:
697 # only update one row
698 y=lineHeight
699 for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)):
700 if self.fSongs[row]._data['id']==self.redrawID:
701 self._paintPlaylistRow(p, row, y, width)
702 y+=lineHeight
704 self.redrawID=None
705 return
707 # paint the headers!
708 p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), self.palette.brush(QPalette.Button))
709 p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1))
710 x=margin+self.xOffset
711 p.setPen(self.palette.color(QPalette.ButtonText))
712 for hdr in self.headers:
713 if hdr[2]:
714 p.drawText(x, vmargin, hdr[1], lineHeight, QtCore.Qt.AlignLeft, hdr[0])
715 x+=hdr[1]
716 p.drawLine(QtCore.QPoint(x-margin,0), QtCore.QPoint(x-margin,lineHeight))
718 if self.songs==None:
719 return
720 # fill the records!
721 y=lineHeight
722 for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)):
723 self._paintPlaylistRow(p, row, y, width)
724 y+=lineHeight
725 if y<self.height():
726 # if we're short on songs, draw up the remaining area in background color
727 p.fillRect(QtCore.QRect(0,y,width,self.height()-y), QtGui.QBrush(self.clrBg))
729 def _paintPlaylistRow(self, p, row, y, width):
730 """Paint row $row on $p on height $y and with width $width."""
731 song=self.fSongs[row]
732 lineHeight=self.lineHeight
733 margin=self.margin
734 vmargin=self.vmargin
735 id=song._data['id']
737 # determine color of row. Default is row-color, but can be overridden by
738 # (in this order): selection, special row color!
739 clr=self.colors[row%2] # background color of the row
740 clrTxt = self.palette.color(QPalette.Text) # color of the printed text
741 # is it selected?
742 values=[]
743 if self.selMode:
744 checkID=row
745 values=self.selRows
746 else:
747 checkID=id
748 values=self.selIDs
749 # if values==[], it won't run!
750 for range in values:
751 # is selected if in range, which depends on the selection-mode
752 if checkID>=min(range) and checkID<=max(range):
753 clr=self.clrSel
754 clrTxt = self.palette.color(QPalette.HighlightedText) # color of the printed text
755 # it has a VIP-status!
756 if id==int(self.clrID[0]):
757 clrTxt = self.palette.color(QPalette.HighlightedText) # color of the printed text
758 clr=self.clrID[1]
760 # draw the row background
761 p.fillRect(QtCore.QRect(2, y, width-3, lineHeight), QtGui.QBrush(clr))
763 # draw a subtile rectangle
764 p.setPen(self.palette.color(QPalette.Highlight))
765 p.drawRect(QtCore.QRect(2, y, width-3, lineHeight))
767 # draw the column
768 x=margin+self.xOffset
769 for hdr in self.headers:
770 if hdr[2]:
771 # only if visible, duh!
772 # rectangle we're allowed to print in
773 text=song.getTag(hdr[0])
774 if type(text)!=str and type(text)!=unicode:
775 text=str(text)
776 rect=p.boundingRect(x, y, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text)
777 p.setPen(clrTxt)
778 p.drawText(x, y+vmargin, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text)
779 if rect.width()>hdr[1]-margin:
780 # print ellipsis, if necessary
781 p.fillRect(x+hdr[1]-15,y+1,15,lineHeight-1, QtGui.QBrush(clr))
782 p.drawText(x+hdr[1]-15,y+vmargin,15,lineHeight-1, QtCore.Qt.AlignLeft, "...")
783 x+=hdr[1]
784 p.setPen(self.palette.color(QPalette.Base))
785 p.drawLine(QtCore.QPoint(x-margin,y), QtCore.QPoint(x-margin,y+lineHeight))
787 def libFirstVisRowIndex(self):
788 """Returns the index of the first visible row in library mode."""
789 # if not in library mode, the unthinkable might happen! Wooo!
790 # TODO find better initial value
791 row=0 # the visible rows we're visiting
792 index=0 # what index does the current row have
793 entries=self.fSongs
795 while index<len(entries):
796 if row>=self.topRow:
797 break
798 entry=entries[index]
799 if entry[LIB_EXPANDED]==0:
800 index=entry[LIB_NEXTROW]
801 else:
802 index+=1
803 row+=1
804 return index
805 def libIthVisRowIndex(self, index, i=1):
806 """Returns the index of the $i-th next row after $index that is visible (or -1) in library mode."""
807 entries=self.fSongs
808 while i>0 and index<len(entries):
809 i-=1
810 entry=self.fSongs[index]
811 if entry[LIB_EXPANDED]==0:
812 if index<0:
813 return -1
814 index=entry[LIB_NEXTROW]
815 else:
816 index+=1
818 return index
821 def libPrint(self):
822 for entry in self.fSongs:
823 indent=""
824 for i in xrange(entry[2]):
825 indent=("%s ")%(indent)
826 print "%s%s" % (indent, entry)
828 def _paintLibrary(self, p):
829 width=self.width()
830 height=self.height()
831 lineHeight=self.lineHeight
832 margin=self.margin
833 vmargin=self.vmargin
835 # paint the headers!
836 p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), self.palette.brush(QPalette.Button))
837 p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1))
838 p.setPen(self.palette.color(QPalette.ButtonText))
839 p.drawText(margin, vmargin, width, lineHeight, QtCore.Qt.AlignLeft, self.groupByStr.replace('$', ''))
841 entries=self.fSongs
843 y=lineHeight
844 x=margin
845 indent=self.indentation
846 index=self.libFirstVisRowIndex()
847 row=0
848 while index<len(entries) and y<height:
849 entry=entries[index]
851 level=entry[LIB_INDENT]
852 isSong=isinstance(entry[LIB_VALUE], Song)
854 if isSong:
855 prefix=''
856 text="%s %s"%(entry[LIB_VALUE].getTrack(), entry[LIB_VALUE].getTitle())
857 else:
858 if entry[LIB_EXPANDED]==1: prefix='-'
859 elif entry[LIB_EXPANDED]==0: prefix='+'
860 text=entry[LIB_VALUE]
862 clr=self.colors[row%2] # background color of the row
863 clrTxt = self.palette.color(QPalette.Text)
865 values=[]
866 if self.selMode:
867 checkID=index
868 values=self.selRows
869 elif self.selMode==False and isSong:
870 checkID=entry[LIB_VALUE].getID()
871 values=self.selIDs
873 # if values==[], then it won't run!
874 for range in values:
875 # is selected if in range, which depends on the selection-mode
876 if checkID>=min(range) and checkID<=max(range):
877 clr=self.clrSel
878 clrTxt = self.palette.color(QPalette.HighlightedText)
880 for i in self.selMiscs:
881 if index==i:
882 clr=self.clrSel
883 clrTxt = self.palette.color(QPalette.HighlightedText)
885 # it has a VIP-status!
886 if isSong and entry[LIB_VALUE].getID()==int(self.clrID[0]):
887 clrTxt = self.palette.color(QPalette.HighlightedText)
888 clr=self.clrID[1]
890 left=x+indent*(1+level)
891 top=y+vmargin
892 p.fillRect(QtCore.QRect(left,y,width-3,lineHeight), clr)
893 p.setPen(clrTxt)
894 p.drawText(left, top, 15, lineHeight, QtCore.Qt.AlignLeft, prefix)
895 p.drawText(left+15, top, width, lineHeight, QtCore.Qt.AlignLeft, text)
897 obj=None
898 if level<len(self.levels):
899 if self.levels[level][0:len('$artist')]=='$artist':
900 obj=self.wgGfxArtist
901 elif self.levels[level][0:len('$album')]=='$album':
902 obj=self.wgGfxAlbum
904 if obj:
905 obj.render(p, QtCore.QRectF(indent*level+1,y+1,lineHeight-1,lineHeight-1))
907 y+=lineHeight
908 row+=1
909 index=self.libIthVisRowIndex(index)
910 if index<0:
911 break
913 _mutex=QtCore.QMutex()
914 _paintCnt=0
915 def paintEvent(self, event):
916 self._mutex.lock()
917 if self._paintCnt:
918 self._mutex.unlock()
919 return
920 self._paintCnt=1
921 self._mutex.unlock()
924 p=QtGui.QPainter(self)
926 # for the moment, redraw everything ...
927 p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(self.clrBg))
928 if self.mode=='playlist':
929 self._paintPlaylist(p)
930 elif self.mode=='library':
931 self._paintLibrary(p)
933 # draw a nice line around the widget!
934 p.drawRect(QtCore.QRect(0,0,self.width()-1,self.height()-1))
935 if self.hasFocus():
936 p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3))
937 else:
938 p.setPen(self.palette.color(QPalette.Button))
939 p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3))
941 self._paintCnt=0
943 text=None
944 if len(self._filters):
945 text="Please wait while filtering ... (%i)"%(len(self._filters))
946 #text='%s - %s' % (self.selMiscs, '')
947 #text='%s - %s - %s' % (str(self.topRow), str(self.selRows), str(self.selIDs))
948 #text='%s - %s'%(str(self.topRow), str(self.numVisEntries))
949 if text:
950 r=QtCore.QRect(10,self.height()-40,self.width()-20,20)
951 p.fillRect(r, self.palette.brush(QPalette.Base))
952 p.drawText(r,QtCore.Qt.AlignLeft, text)