Library: use QTreeWidget.
[nephilim.git] / wgSongList.py
blob6170142e87e274af9312b855ffabdf050f5b7b0e
1 from PyQt4 import QtGui, QtCore, QtSvg
2 from PyQt4.QtGui import QPalette
4 import sys
5 from traceback import print_exc
7 from misc import *
8 from clSong import Song
9 from clSettings import settings
10 import format
12 # constants used for fSongs
13 LIB_ROW=0
14 LIB_VALUE=1
15 LIB_INDENT=2
16 LIB_NEXTROW=3
17 LIB_EXPANDED=4 # values: 0, 1 or 2 (==song)
18 LIB_PARENT=5
20 class DoUpdate(QtCore.QEvent):
21 def __init__(self):
22 QtCore.QEvent.__init__(self, QtCore.QEvent.User)
23 class DoResize(QtCore.QEvent):
24 def __init__(self):
25 QtCore.QEvent.__init__(self, QtCore.QEvent.User)
27 class SongList(QtGui.QWidget):
28 """The SongList widget is a list optimized for displaying an array of songs, with filtering option."""
29 # CONFIGURATION VARIABLES
30 " font size in pxl"
31 fontSize=12 #TODO:make this selectable
32 " height of line in pxl"
33 lineHeight = fontSize + 4
34 " margin"
35 margin=4
36 vmargin=(lineHeight-fontSize)/2-1
37 " width of the vscrollbar"
38 scrollbarWidth=15
39 " minimum column width"
40 minColumnWidth=50
41 " colors for alternating rows"
42 colors = []
43 " color of selection"
44 clrSel = None
45 " background color"
46 clrBg = None
47 " indentation of hierarchy, in pixels"
48 indentation=lineHeight
50 " what function to call when the list is double clicked"
51 onDoubleClick=None
53 mode='playlist' # what mode is the songlist in? values: 'playlist', 'library'
54 " the headers: ( (header, width, visible)+ )"
55 headers=None
56 songs=None # original songs
57 numSongs=None # number of songs
59 # 'edited' songs
60 # in playlist mode, this can only filtering
61 # in library mode, this indicates all entries: (row, tag-value, indentation, next-row, expanded)*
62 fSongs=None # filtered songs
63 numVisEntries=None # number of entries that are visible (including when scrolling)
66 levels=[] # levels from the groupBy in library-mode
67 groupByStr='' # groupBy used in library-mode
69 vScrollbar=None
70 hScrollbar=None
72 topRow=-1
73 numRows=-1 # total number of rows that can be visible in 1 time
74 selRows=None # ranges of selected rows: ( (startROw,endRow)* )
75 selIDs=None # ranges of selected IDs: [ [startID,endID] ]
76 selMiscs=None # array of indexes for selected non-songs in library mode
78 selMode=False # currently in select mode?
79 resizeCol=None # resizing a column?
80 clrID=None # do we have to color a row with certain ID? [ID, color]
81 scrollMult=1 # how many rows do we jump when scrolling by dragging
82 xOffset=0 # offset for drawing. Is changed by hScrollbar
83 resizeColumn=None # indicates this column should be recalculated
84 redrawID=None # redraw this ID/row only
86 wgGfxAlbum = None
87 wgGfxArtist = None
89 def __init__(self, parent, name, headers, onDoubleClick):
90 QtGui.QWidget.__init__(self, parent)
91 self.onDoubleClick=onDoubleClick
93 # we receive an array of strings; we convert that to an array of (header, width)
94 self.name=name
95 # load the headers, and fetch from the settings the width and visibility
96 self.headers=map(lambda h: [h, int(settings.get('l%s.%s.width'%(self.name,h),250))
97 , settings.get('l%s.%s.visible'%(self.name,h),'1')=='1'], headers)
98 self.headers.insert(0, ['id', 30, settings.get('l%s.%s.visible'%(self.name,'id'),'0')=='1'])
99 self.songs=None
100 self.numSongs=None
101 self.fSongs=None
102 self.selMiscs=[]
103 self.numVisEntries=None
104 self.xOffset=0
105 self.resizeColumn=None
107 self.colors = [self.palette().color(QPalette.Base), self.palette().color(QPalette.AlternateBase)]
108 self.clrSel = self.palette().color(QPalette.Highlight)
109 self.clrBg = self.palette().color(QPalette.Window)
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)
145 self.wgGfxAlbum=QtSvg.QSvgRenderer('gfx/gnome-cd.svg')
146 self.wgGfxArtist=QtSvg.QSvgRenderer('gfx/user_icon.svg')
148 def sizeHint(self):
149 return QtCore.QSize(10000,10000)
151 def getSongs(self):
152 return self.songs
154 def customEvent(self, event):
155 if isinstance(event, DoResize):
156 self.resizeEvent(None)
157 self.update()
158 elif isinstance(event, DoUpdate):
159 self.update()
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 level in self.levels:
185 formats.append(format.compile(level))
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 if songs:
253 self.numSongs = len(songs)
254 else:
255 self.numSongs = 0
257 self.setMode(self.mode, self.groupByStr)
259 self.redrawID=None
260 QtCore.QCoreApplication.postEvent(self, DoUpdate())
262 def selectedSongs(self):
263 """Returns the list of selected songs."""
264 ret=[]
265 if self.mode=='playlist':
266 cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1]
267 elif self.mode=='library':
268 cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1]
269 for range in self.selIDs:
270 # look for the songs in the current range
271 songs=filter(cmp, self.songs)
272 # add songs in range
273 ret.extend(songs)
274 return ret
276 def killFilters(self):
277 songs=self.songs
278 self.songs=None
279 while (len(self._filters)):
280 # wait 'till everything's cleared
281 doEvents()
282 self.songs=songs
285 # contains filters ready to be applied; only the top one will be used
286 _filters=[]
287 def filter(self, strFilter):
288 """Filter songs according to $strFilter."""
290 self._filters.append(strFilter)
291 strFilter=self._filters[-1]
293 try:
294 if self.mode=='playlist':
295 self.fSongs=filter(lambda song: song.match(strFilter), self.songs)
296 self.numVisEntries=len(self.fSongs)
297 else:
298 self.groupBy(self.groupByStr, strFilter)
299 except:
300 # we might get here because self.songs is None
301 pass
302 self._filters=[]
303 QtCore.QCoreApplication.postEvent(self, DoResize())
305 def colorID(self, id, clr):
306 """Color the row which contains song with id $id $clr."""
307 self.clrID=[id, clr]
308 self.redrawID=id
310 QtCore.QCoreApplication.postEvent(self, DoUpdate())
312 def selectRow(self, row):
313 """Make $row the current selection."""
314 self.selRows=[[row,row]]
316 QtCore.QCoreApplication.postEvent(self, DoUpdate())
318 def showColumn(self, column, show=True):
319 """Hide or show column $column."""
320 self.headers[column][2]=show
322 self.update()
324 def autoSizeColumn(self, column):
325 """Resizes column $column to fit the widest entry in the non-filtered songs."""
326 # we can't calculate it here, as retrieving the text-width can only
327 # be done in the paintEvent method ...
328 self.resizeColumn=column
330 QtCore.QCoreApplication.postEvent(self, DoUpdate())
332 def visibleSongs(self):
333 """Get the songs currently visible."""
334 ret=[]
335 if self.mode=='playlist':
336 for row in xrange(self.topRow, min(self.numSongs, self.topRow+self.numRows)-1):
337 ret.append(self.fSongs[row])
338 elif self.mode=='library':
339 # note that if everything is folded, there'll be no songs!
340 entries=self.fSongs
341 index=self.libFirstVisRowIndex()
342 count=0
343 while index>=0 and index<len(entries) and count<self.numRows:
344 entry=self.fSongs[index]
345 if isinstance(entry[LIB_VALUE], Song):
346 ret.append(entry[LIB_VALUE])
347 index=self.libIthVisRowIndex(index)
348 count+=1
349 return ret
351 def ensureVisible(self, id):
352 """Make sure the song with $id is visible."""
353 if len(filter(lambda song: song.getID()==id, self.visibleSongs())):
354 return
355 row=0
356 ok=False
357 if self.mode=='playlist':
358 # playlist mode is simple: just hop to the song with id!
359 for song in self.fSongs:
360 row+=1
361 if song.getID()==id:
362 ok=True
363 break
364 elif self.mode=='library':
365 # library mode is more complex: we must find out how many rows are visible,
366 # and expand the parents of the song, if necessary
367 indLevel=0 # indicates what is the deepest level that is expanded for the current entry
368 # thus if current indent<=indLevel, then it is visible
369 for entry in self.fSongs:
370 if entry[LIB_EXPANDED]==1:
371 indLevel=max(entry[LIB_INDENT]+1, indLevel)
372 elif entry[LIB_EXPANDED]==0:
373 indLevel=min(entry[LIB_INDENT], indLevel)
374 if entry[LIB_INDENT]<=indLevel:
375 row+=1
377 #print "%s -> %s"%(str(indLevel), str(entry))
378 if isinstance(entry[LIB_VALUE], Song) and entry[LIB_VALUE].getID()==id:
379 # expand parents
380 # must be expanded in reverse order, else we will count too many
381 # entries ...
382 parents=[]
383 while entry[LIB_PARENT]>=0:
384 entry=self.fSongs[entry[LIB_PARENT]]
385 parents.append(entry)
386 row-=1
387 parents.reverse()
388 for parent in parents:
389 self.libExpand(parent)
390 ok=True
391 break
393 if ok:
394 self.vScrollbar.setValue(row-self.numRows/2)
395 self.update()
398 def onVScroll(self, value):
399 # 'if value<0' needed because minimum can be after init <0 at some point ...
400 if value<0: value=0
401 if value>self.numVisEntries:value=self.numVisEntries
402 self.topRow=value
404 self.update()
406 def onHScroll(self, value):
407 self.xOffset=-self.hScrollbar.value()*2
408 self.update()
410 def _pos2row(self, pos):
411 return int(pos.y()/self.lineHeight)-1
412 def _row2entry(self, row):
413 entry=self.fSongs[0]
414 try:
415 while row>0:
416 if entry[LIB_EXPANDED]:
417 entry=self.fSongs[entry[LIB_ROW]+1]
418 else:
419 entry=self.fSongs[entry[LIB_NEXTROW]]
420 row-=1
421 except:
422 return None
423 return entry
425 def focusOutEvent(self, event):
426 self.update()
427 def focusInEvent(self, event):
428 self.update()
429 def wheelEvent(self, event):
430 if self.vScrollbar.isVisible():
431 event.accept()
432 numDegrees=event.delta() / 8
433 numSteps=5*numDegrees/15
434 self.vScrollbar.setValue(self.vScrollbar.value()-numSteps)
436 def resizeEvent(self, event):
437 # max nr of rows shown
438 self.numRows=int(self.height()/self.lineHeight)
440 # check vertical scrollbar
441 if self.numRows>self.numVisEntries:
442 self.vScrollbar.setVisible(False)
443 self.vScrollbar.setValue(0)
444 else:
445 self.vScrollbar.setVisible(True)
446 self.vScrollbar.setPageStep(self.numRows-2)
447 self.vScrollbar.setMinimum(0)
448 self.vScrollbar.setMaximum(self.numVisEntries-self.numRows+1)
449 self.vScrollbar.resize(self.scrollbarWidth, self.height()-self.lineHeight-1)
450 self.vScrollbar.move(self.width()-self.vScrollbar.width()-1, self.lineHeight-1)
452 # check horizontal scrollbar
453 self.scrollWidth=0
454 if self.mode=='playlist':
455 for hdr in self.headers:
456 if hdr[2]:
457 self.scrollWidth+=hdr[1]
459 if self.scrollWidth>self.width():
460 self.hScrollbar.setVisible(True)
461 self.hScrollbar.setMinimum(0)
462 self.hScrollbar.setMaximum((self.scrollWidth-self.width())/2)
463 self.hScrollbar.resize(self.width()-4, self.scrollbarWidth)
464 self.hScrollbar.move(2, self.height()-self.hScrollbar.height()-1)
466 # some changes because the hScrollbar takes some vertical space ...
467 self.vScrollbar.resize(self.vScrollbar.width(), self.vScrollbar.height()-self.lineHeight)
468 self.vScrollbar.setMaximum(self.vScrollbar.maximum()+1)
470 self.numRows-=1
471 else:
472 self.hScrollbar.setVisible(False)
473 self.hScrollbar.setValue(0)
475 def libExpand(self, entry):
476 if entry and entry[LIB_EXPANDED]==0:
477 self.libToggle(entry)
478 def libCollapse(self, entry):
479 if entry and entry[LIB_EXPANDED]==1:
480 self.libToggle(entry)
482 def libToggle(self, entry):
483 """Toggles expanded state. Returns new state"""
484 expanded=entry[LIB_EXPANDED]
485 if expanded!=2:
486 # there was a '+' or a '-'!
487 entry[LIB_EXPANDED]=(expanded+1)%2
488 ret=entry[LIB_EXPANDED]
489 # we must find out how many entries have appeared/disappeard
490 # while collapsing.
491 visibles=0 # how many new elements have appeared?
492 i=entry[LIB_ROW]+1 # current element looking at
493 indLevel=self.fSongs[i][LIB_INDENT]
494 while i<=entry[LIB_NEXTROW]-1 and i<len(self.fSongs):
495 entry2=self.fSongs[i]
496 if entry2[LIB_EXPANDED]==1:
497 indLevel=max(entry2[LIB_INDENT]+1, indLevel)
498 elif entry2[LIB_EXPANDED]==0:
499 indLevel=min(entry2[LIB_INDENT], indLevel)
500 if entry2[LIB_INDENT]<=indLevel:
501 visibles+=1
503 if entry2[LIB_EXPANDED]==0:
504 i=entry2[LIB_NEXTROW]
505 else:
506 i+=1
508 mult=1
509 if ret==0: mult=-1
510 self.numVisEntries+=mult*visibles
511 self.resizeEvent(None)
512 return ret
514 return 2
516 def mousePressEvent(self, event):
517 self.setFocus()
518 pos=event.pos()
519 row=self._pos2row(pos)
521 done=False # indicates whether some action has been done or not
522 if self.mode=='playlist':
523 self.scrollMult=1
524 if row==-1:
525 # we're clicking in the header!
526 self.resizeCol=None
527 x=0+self.xOffset
529 # check if we're clicking between two columns, if so: resize mode!
530 for hdr in self.headers:
531 if hdr[2]:
532 x+=hdr[1]
533 if abs(x-pos.x())<4:
534 self.resizeCol=i
535 done=True
536 i+=1
537 elif self.mode=='library':
538 entry=self._row2entry(row+self.topRow)
539 if not entry:
540 entry=self.fSongs[len(self.fSongs)-1]
541 if entry and pos.x()>(1+entry[LIB_INDENT])*self.indentation \
542 and pos.x()<(1+entry[LIB_INDENT]+3/2)*self.indentation:
543 # we clicked in the margin, to expand or collapse
544 self.libToggle(entry)
545 done=True
547 if done==False:
548 self.selMode=True
549 self.selIDs=[]
550 self.selMiscs=[]
551 if row==-1 and self.resizeCol==None:
552 # we're not resizing, thus we can select all!
553 self.selRows=[[0, len(self.fSongs)]]
554 elif row>=0:
555 # we start selection mode
556 if self.mode=='playlist':
557 self.selRows=[[self.topRow+row,self.topRow+row]]
558 elif self.mode=='library':
559 self.selRows=[[entry[LIB_ROW], entry[LIB_NEXTROW]-1]]
560 self.selMode=True
562 self.update()
564 def mouseMoveEvent(self, event):
565 pos=event.pos()
566 row=self._pos2row(pos)
567 if self.selMode:
568 # we're in selection mode
569 if row<0:
570 # scroll automatically when going out of the widget
571 row=0
572 if self.topRow>0:
573 self.scrollMult+=0.1
574 jump=int(self.scrollMult)*int(abs(pos.y())/self.lineHeight)
575 self.vScrollbar.setValue(self.vScrollbar.value()-jump)
576 row=jump
577 elif row>=self.numRows:
578 # scroll automatically when going out of the widget
579 self.scrollMult+=0.1
580 jump=int(self.scrollMult)*int(abs(self.height()-pos.y())/self.lineHeight)
581 self.vScrollbar.setValue(self.vScrollbar.value()+jump)
582 row=self.numRows-jump
583 else:
584 # reset the scrollMultiplier
585 self.scrollMult=1
587 if self.mode=='playlist':
588 self.selRows[0][1]=row+self.topRow
589 elif self.mode=='library':
590 self.selRows[0][1]=self.libIthVisRowIndex(self.libIthVisRowIndex(0,self.topRow), row)
591 self.update()
592 elif self.resizeCol!=None:
593 row-=1
594 # ohla, we're resizing a column!
595 prev=0
596 # calculate where we are
597 for i in xrange(self.resizeCol):
598 hdr=self.headers[i]
599 if hdr[2]:
600 prev+=hdr[1]
601 self.headers[self.resizeCol][1]=pos.x()-prev-self.xOffset
602 # minimum width check?
603 if self.headers[self.resizeCol][1]<self.minColumnWidth:
604 self.headers[self.resizeCol][1]=self.minColumnWidth
605 self.resizeEvent(None)
606 self.update()
608 def mouseReleaseEvent(self, event):
609 if self.selMode and len(self.selRows):
610 # we were selecting, but now we're done.
611 # We have to transform one range of rows
612 # into range of selected IDs
613 # The problem is that the list can be filtered, and that
614 # consequtive, visible rows aren't always directly
615 # consequtive in the unfiltered list.
616 self.selMode=False # exit selection mode
617 fSongs=self.fSongs
618 self.selMiscs=[]
619 ranges=[]
620 curRange=[]
621 # loop over all rows that are selected
622 for entry in fSongs[min(self.selRows[0]):max(self.selRows[0])+1]:
623 song=None
624 if isinstance(entry,Song):
625 song=entry
626 elif isinstance(entry[LIB_VALUE],Song):
627 song=entry[LIB_VALUE]
628 else:
629 self.selMiscs.append(entry[LIB_ROW])
631 if song!=None:
632 id=song.getID()
633 # is this song directly after the previous row?
634 if len(curRange)==0 or curRange[-1]+1==id:
635 curRange.append(id)
636 else:
637 ranges.append(curRange)
638 curRange=[id]
639 if len(curRange):
640 ranges.append(curRange)
641 # clean up ranges
642 self.selRows=[]
643 self.selIDs=[]
644 for range in ranges:
645 self.selIDs.append([range[0], range[-1]])
646 self.update()
648 elif self.resizeCol!=None:
649 # store this new width!
650 self.saveColumnWidth(self.resizeCol)
651 # we're not resizing anymore!
652 self.resizeCol=None
653 self.update()
654 def saveColumnWidth(self, col):
655 settings.set('l%s.%s.width'%(self.name,self.headers[col][0]), self.headers[col][1])
656 def mouseDoubleClickEvent(self, event):
657 pos=event.pos()
658 row=self._pos2row(pos)
659 if row>=0:
660 self.onDoubleClick()
661 else:
662 # auto-size column
663 x=0+self.xOffset
665 for hdr in self.headers:
666 if hdr[2]:
667 x+=hdr[1]
668 if abs(x-pos.x())<4:
669 self.autoSizeColumn(i)
670 break
671 i+=1
673 def _paintPlaylist(self, p):
674 self.redrawID=None
677 lineHeight=self.lineHeight
678 margin=self.margin
679 vmargin=self.vmargin
680 selRows=self.selRows
681 width=self.width()
682 if self.vScrollbar.isVisible():
683 width-=self.scrollbarWidth
685 if self.resizeColumn!=None:
686 # we're autoresizing!
687 # must be done here, because only here we can check the textwidth!
688 # This is because of limitations it can be only be done in paintEvent
689 hdr=self.headers[self.resizeColumn][0]
690 w=self.minColumnWidth
691 # loop over all visible songs ...
692 for song in self.fSongs:
693 rect=p.boundingRect(10,10,1,1, QtCore.Qt.AlignLeft, song.getTag(hdr))
694 w=max(rect.width(), w)
695 self.headers[self.resizeColumn][1]=w+2*margin
696 self.saveColumnWidth(self.resizeColumn)
697 self.resizeColumn=None
698 self.resizeEvent(None)
699 if self.redrawID!=None:
700 # only update one row
701 y=lineHeight
702 for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)):
703 if self.fSongs[row]._data['id']==self.redrawID:
704 self._paintPlaylistRow(p, row, y, width)
705 y+=lineHeight
707 self.redrawID=None
708 return
710 # paint the headers!
711 p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), self.palette().brush(QPalette.Button))
712 p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1))
713 x=margin+self.xOffset
714 p.setPen(self.palette().color(QPalette.ButtonText))
715 for hdr in self.headers:
716 if hdr[2]:
717 p.drawText(x, vmargin, hdr[1], lineHeight, QtCore.Qt.AlignLeft, hdr[0])
718 x+=hdr[1]
719 p.drawLine(QtCore.QPoint(x-margin,0), QtCore.QPoint(x-margin,lineHeight))
721 if self.songs==None:
722 return
723 # fill the records!
724 y=lineHeight
725 for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)):
726 self._paintPlaylistRow(p, row, y, width)
727 y+=lineHeight
728 if y<self.height():
729 # if we're short on songs, draw up the remaining area in background color
730 p.fillRect(QtCore.QRect(0,y,width,self.height()-y), QtGui.QBrush(self.clrBg))
732 def _paintPlaylistRow(self, p, row, y, width):
733 """Paint row $row on $p on height $y and with width $width."""
734 song=self.fSongs[row]
735 lineHeight=self.lineHeight
736 margin=self.margin
737 vmargin=self.vmargin
738 id=song._data['id']
740 # determine color of row. Default is row-color, but can be overridden by
741 # (in this order): selection, special row color!
742 clr=self.colors[row%2] # background color of the row
743 clrTxt = self.palette().color(QPalette.Text) # color of the printed text
744 # is it selected?
745 values=[]
746 if self.selMode:
747 checkID=row
748 values=self.selRows
749 else:
750 checkID=id
751 values=self.selIDs
752 # if values==[], it won't run!
753 for range in values:
754 # is selected if in range, which depends on the selection-mode
755 if checkID>=min(range) and checkID<=max(range):
756 clr=self.clrSel
757 clrTxt = self.palette().color(QPalette.HighlightedText) # color of the printed text
758 # it has a VIP-status!
759 if id==int(self.clrID[0]):
760 clrTxt = self.palette().color(QPalette.HighlightedText) # color of the printed text
761 clr=self.clrID[1]
763 # draw the row background
764 p.fillRect(QtCore.QRect(2, y, width-3, lineHeight), QtGui.QBrush(clr))
766 # draw a subtile rectangle
767 p.setPen(self.palette().color(QPalette.Highlight))
768 p.drawRect(QtCore.QRect(2, y, width-3, lineHeight))
770 # draw the column
771 x=margin+self.xOffset
772 for hdr in self.headers:
773 if hdr[2]:
774 # only if visible, duh!
775 # rectangle we're allowed to print in
776 text=song.getTag(hdr[0])
777 if type(text)!=str and type(text)!=unicode:
778 text=str(text)
779 rect=p.boundingRect(x, y, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text)
780 p.setPen(clrTxt)
781 p.drawText(x, y+vmargin, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text)
782 if rect.width()>hdr[1]-margin:
783 # print ellipsis, if necessary
784 p.fillRect(x+hdr[1]-15,y+1,15,lineHeight-1, QtGui.QBrush(clr))
785 p.drawText(x+hdr[1]-15,y+vmargin,15,lineHeight-1, QtCore.Qt.AlignLeft, "...")
786 x+=hdr[1]
787 p.setPen(self.palette().color(QPalette.Base))
788 p.drawLine(QtCore.QPoint(x-margin,y), QtCore.QPoint(x-margin,y+lineHeight))
790 def libFirstVisRowIndex(self):
791 """Returns the index of the first visible row in library mode."""
792 # if not in library mode, the unthinkable might happen! Wooo!
793 # TODO find better initial value
794 row=0 # the visible rows we're visiting
795 index=0 # what index does the current row have
796 entries=self.fSongs
798 while index<len(entries):
799 if row>=self.topRow:
800 break
801 entry=entries[index]
802 if entry[LIB_EXPANDED]==0:
803 index=entry[LIB_NEXTROW]
804 else:
805 index+=1
806 row+=1
807 return index
808 def libIthVisRowIndex(self, index, i=1):
809 """Returns the index of the $i-th next row after $index that is visible (or -1) in library mode."""
810 entries=self.fSongs
811 while i>0 and index<len(entries):
812 i-=1
813 entry=self.fSongs[index]
814 if entry[LIB_EXPANDED]==0:
815 if index<0:
816 return -1
817 index=entry[LIB_NEXTROW]
818 else:
819 index+=1
821 return index
823 def _paintLibrary(self, p):
824 width=self.width()
825 height=self.height()
826 lineHeight=self.lineHeight
827 margin=self.margin
828 vmargin=self.vmargin
830 # paint the headers!
831 p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), self.palette().brush(QPalette.Button))
832 p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1))
833 p.setPen(self.palette().color(QPalette.ButtonText))
834 p.drawText(margin, vmargin, width, lineHeight, QtCore.Qt.AlignLeft, self.groupByStr.replace('$', ''))
836 entries=self.fSongs
838 y=lineHeight
839 x=margin
840 indent=self.indentation
841 index=self.libFirstVisRowIndex()
842 row=0
843 while index<len(entries) and y<height:
844 entry=entries[index]
846 level=entry[LIB_INDENT]
847 isSong=isinstance(entry[LIB_VALUE], Song)
849 if isSong:
850 prefix=''
851 text="%s %s"%(entry[LIB_VALUE].getTrack(), entry[LIB_VALUE].getTitle())
852 else:
853 if entry[LIB_EXPANDED]==1: prefix='-'
854 elif entry[LIB_EXPANDED]==0: prefix='+'
855 text=entry[LIB_VALUE]
857 clr=self.colors[row%2] # background color of the row
858 clrTxt = self.palette().color(QPalette.Text)
860 values=[]
861 if self.selMode:
862 checkID=index
863 values=self.selRows
864 elif self.selMode==False and isSong:
865 checkID=entry[LIB_VALUE].getID()
866 values=self.selIDs
868 # if values==[], then it won't run!
869 for range in values:
870 # is selected if in range, which depends on the selection-mode
871 if checkID>=min(range) and checkID<=max(range):
872 clr=self.clrSel
873 clrTxt = self.palette().color(QPalette.HighlightedText)
875 for i in self.selMiscs:
876 if index==i:
877 clr=self.clrSel
878 clrTxt = self.palette().color(QPalette.HighlightedText)
880 # it has a VIP-status!
881 if isSong and entry[LIB_VALUE].getID()==int(self.clrID[0]):
882 clrTxt = self.palette().color(QPalette.HighlightedText)
883 clr=self.clrID[1]
885 left=x+indent*(1+level)
886 top=y+vmargin
887 p.fillRect(QtCore.QRect(left,y,width-3,lineHeight), clr)
888 p.setPen(clrTxt)
889 p.drawText(left, top, 15, lineHeight, QtCore.Qt.AlignLeft, prefix)
890 p.drawText(left+15, top, width, lineHeight, QtCore.Qt.AlignLeft, text)
892 obj=None
893 if level<len(self.levels):
894 if self.levels[level][0:len('$artist')]=='$artist':
895 obj=self.wgGfxArtist
896 elif self.levels[level][0:len('$album')]=='$album':
897 obj=self.wgGfxAlbum
899 if obj:
900 obj.render(p, QtCore.QRectF(indent*level+1,y+1,lineHeight-1,lineHeight-1))
902 y+=lineHeight
903 row+=1
904 index=self.libIthVisRowIndex(index)
905 if index<0:
906 break
908 _mutex=QtCore.QMutex()
909 _paintCnt=0
910 def paintEvent(self, event):
911 self._mutex.lock()
912 if self._paintCnt:
913 self._mutex.unlock()
914 return
915 self._paintCnt=1
916 self._mutex.unlock()
919 p=QtGui.QPainter(self)
921 # for the moment, redraw everything ...
922 p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(self.clrBg))
923 if self.mode=='playlist':
924 self._paintPlaylist(p)
925 elif self.mode=='library':
926 self._paintLibrary(p)
928 # draw a nice line around the widget!
929 p.drawRect(QtCore.QRect(0,0,self.width()-1,self.height()-1))
930 if self.hasFocus():
931 p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3))
932 else:
933 p.setPen(self.palette().color(QPalette.Button))
934 p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3))
936 self._paintCnt=0
938 text=None
939 if len(self._filters):
940 text="Please wait while filtering ... (%i)"%(len(self._filters))
941 #text='%s - %s' % (self.selMiscs, '')
942 #text='%s - %s - %s' % (str(self.topRow), str(self.selRows), str(self.selIDs))
943 #text='%s - %s'%(str(self.topRow), str(self.numVisEntries))
944 if text:
945 r=QtCore.QRect(10,self.height()-40,self.width()-20,20)
946 p.fillRect(r, self.palette().brush(QPalette.Base))
947 p.drawText(r,QtCore.Qt.AlignLeft, text)