diff options
author | jerous <jerous@gmail.com> | 2008-08-23 17:49:06 +0200 |
---|---|---|
committer | jerous <jerous@gmail.com> | 2008-08-23 17:49:06 +0200 |
commit | aafd9acd552099623204b2249e2c70b3cf27f95a (patch) | |
tree | 318edf6b078a8f9978028b494cfd25ebbf9798e0 | |
parent | 01f47fffef0ac6bfcf3ab064a6bd3cf26b4f9448 (diff) | |
parent | 49e765eadb497e0fd2d72b6e3c74edb921b072a8 (diff) |
fix wrong things in git
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | clMonty.py | 21 | ||||
-rw-r--r-- | clPlugin.py | 29 | ||||
-rw-r--r-- | clSong.py | 11 | ||||
-rw-r--r-- | jmpc.py | 98 | ||||
-rw-r--r-- | jmpd.py | 219 | ||||
-rw-r--r-- | misc.py | 105 | ||||
-rw-r--r-- | montypc.py | 8 | ||||
-rw-r--r-- | mpdconf | 3 | ||||
-rw-r--r-- | plugins/AlbumCover.py | 20 | ||||
-rw-r--r-- | plugins/Library.py | 6 | ||||
-rw-r--r-- | plugins/Lyrics.py | 69 | ||||
-rw-r--r-- | plugins/Notify.py | 145 | ||||
-rw-r--r-- | plugins/PlayControl.py | 125 | ||||
-rw-r--r-- | plugins/Playlist.py | 5 | ||||
-rw-r--r-- | plugins/Shortcuts.py | 48 | ||||
-rw-r--r-- | plugins/SongStatus.py | 22 | ||||
-rw-r--r-- | plugins/Systray.py | 7 | ||||
-rw-r--r-- | plugins/Tabs.py | 161 | ||||
-rw-r--r-- | plugins/__init__.py | 12 | ||||
-rw-r--r-- | plugins/amazon_cover_fetcher.py (renamed from amazon_cover_fetcher.py) | 0 | ||||
-rw-r--r-- | test.py | 19 | ||||
-rw-r--r-- | wgPlaylist.py | 8 | ||||
-rw-r--r-- | wgSongList.py | 21 | ||||
-rw-r--r-- | winMain.py | 88 | ||||
-rw-r--r-- | winSettings.py | 34 |
26 files changed, 1155 insertions, 130 deletions
@@ -3,3 +3,4 @@ .vimsession settings.txt layout +*.cover @@ -3,7 +3,7 @@ from traceback import * from clSong import Song from traceback import print_exc from misc import * -import mpd +import jmpc from threading import Thread class Monty(QtCore.QObject): @@ -26,6 +26,7 @@ class Monty(QtCore.QObject): _timerID=None events={ + 'beforeSongChange':'curSongID', 'onSongChange':'oldSongID, newSongID', 'onTimeChange':'oldTime, newTime', 'onStateChange':'oldState, newState', @@ -69,7 +70,7 @@ class Monty(QtCore.QObject): self._updateLib() self._updatePlaylist() self._updateCurrentSong() - self._timerID=self.startTimer(300) + self._timerID=self.startTimer(500) except Exception: print_exc() self._raiseEvent('onStateChange', {'oldState':'stop', 'newState':self.getStatus()['state']}) @@ -129,6 +130,11 @@ class Monty(QtCore.QObject): print_exc() return None + def repeat(self,val): + self._client.repeat(val) + def random(self,val): + self._client.random(val) + _retrMutex=QtCore.QMutex() def _retrieve(self, method): """Makes sure only one call is made at a time to MPD.""" @@ -144,6 +150,7 @@ class Monty(QtCore.QObject): def play(self, id): """Play song with ID $id.""" + self._playCalled=True if id!=None: self._client.playid(id) else: @@ -157,7 +164,13 @@ class Monty(QtCore.QObject): self._client.pause(0) def next(self): """Move on to the next song in the playlist.""" - self._client.next() + self._playCalled=False + self._raiseEvent('beforeSongChange', {'curSongID': self._curSongID}) + # we only switch to the next song, if some of beforeSongChange's listeners + # didn't explicitly call play. If it did, then it ain't good to immediatly + # skip to the next song! + if not self._playCalled: + self._client.next() def previous(self): """Move back to the previous song in the playlist.""" self._client.previous() @@ -269,6 +282,8 @@ class Monty(QtCore.QObject): if curTime!=self._curTime: self._raiseEvent('onTimeChange', {'oldTime':self._curTime, 'newTime':curTime}) self._curTime=curTime + if curTime>=status['length']-1: + self._raiseEvent('beforeSongChange', {'curSongID':curID}) " check if the playing state has changed" if 'state' in status: diff --git a/clPlugin.py b/clPlugin.py index 75e2b44..d750bfe 100644 --- a/clPlugin.py +++ b/clPlugin.py @@ -10,12 +10,16 @@ class Plugin: settingsWidget=None settings=None winMain=None + loaded=None def __init__(self, winMain, name): self.name=name self.winMain=winMain + self.loaded=False - def getName(self): + def getName(self, lower=False): + if lower: + return self.name.lower() return self.name def getInfo(self): return '' @@ -26,7 +30,21 @@ class Plugin: def setStatus(self, status): self.winMain.setStatus(status) - def getDockWidget(self, opts): + def load(self): + self._load() + opts=QtGui.QDockWidget.DockWidgetClosable|QtGui.QDockWidget.DockWidgetMovable + self.winMain.addDock(self.getDockWidget(opts)) + self.loaded=True + def unload(self): + self._unload() + self.winMain.removeDock(self.getDockWidget()) + self.dockWidget=None + self.settingsWidget=None + self.loaded=False + def isLoaded(self): + return self.loaded + + def getDockWidget(self, opts=None): try: if not self.dockWidget: self.dockWidget=self._getDockWidget() @@ -103,5 +121,10 @@ class Plugin: dock.setWidget(widget) return dock - + def _load(self): + """Override this one.""" + return + def _unload(self): + """Override this one.""" + return @@ -6,6 +6,17 @@ class Song: self._data=data if 'id' in self._data: self._data['id']=int(self._data['id']) + if 'track' in self._data: + # make sure the track is a valid number! + t=self._data['track'] + for i in xrange(len(t)): + if ord(t[i])<ord('0') or ord(t[i])>ord('9'): + try: + self._data['track']=int(t[0:i]) + except: + self._data['track']=-1 + break + self._data['track']=int(self._data['track']) def getID(self): """Get the ID.""" @@ -0,0 +1,98 @@ +import socket + +class MPDError(Exception): + pass +class ConnectionError(MPDError): + pass +class ProtocolError(MPDError): + pass +class CommandError(MPDError): + pass +class CommandListError(MPDError): + pass + + +class client: + _csock=None + _rfile=None + _wfile=None + def __init__(self): + pass + + def connect(self, host, port, type=socket.SOCK_STREAM): + if self._csock: + raise ConnectionError("Already connected") + + self._csock=socket.socket(socket.AF_INET, type) + self._csock.connect((host, port)) + self._file = self._csock.makefile("rwb") + handshake=self._read() + if handshake[0:len("hello jmpd")]!="hello jmpd": + print("Failed to handshake: %s" % (handshake)) + self.disconnect() + return + self._write("ack") + + + def _read(self): + return self._file.readline().strip() + def _write(self, line): + self._file.write("%s\n" % (line)) + + def disconnect(self): + self._csock.close() + self._csock=None + + def command_list_ok_begin(self): + pass + + def command_list_end(self): + pass + + def update(self): + pass + + def status(self): + pass + + def playid(id): + pass + def stop(self): + pass + + def pause(self): + pass + def resume(self): + pass + + def next(self): + pass + def previous(self): + pass + + def seekid(time): + pass + + # Playlist management + def add(path): + pass + def deleteid(path): + pass + + # volume + def setvol(newvolume): + pass + + # info + def listallinfo(self): + pass + def listplaylistinfo(self): + pass + def currentsong(self): + pass + + +from test import port +client=client() +client.connect('localhost', port) +client.disconnect() @@ -0,0 +1,219 @@ +import socket +from thread import start_new_thread +from traceback import print_exc +import codecs +import os + +from mutagen.easyid3 import EasyID3 + +from clSettings import Settings + +VERSION='0.1' +MUSIC_DIRECTORY="music_directory" +ENCODING="UTF-8" + +class server: + ssocket=None + _sockets=[] + + def __init__(self): + self._jmpd=jmpd() + self._sockets=[] + + def start(self, port, type=socket.SOCK_STREAM): + try: + self.ssocket=socket.socket(socket.AF_INET, type) + self._start_server(port) + except: + print_exc() + return + + def _start_server(self, port): + self.ssocket.bind(('localhost', port)) + self.ssocket.listen(5) + self.ssocket.settimeout(None) + + print("Started jmpd v%s" % (VERSION)) + while True: + csock,caddr=self.ssocket.accept() + self._sockets.append(csock) + #start_new_thread(self.handle, (csock, caddr)) + self.handle(csock, caddr) + + def _read(self, file): + line="" + while line=="": + line=file.readline() + return line.strip() + def _write(self, file, line): + file.write("%s\n" % (line)) + + def handle(self, csocket, caddr): + print("Incoming connection from %s" % (str(caddr))) + file=csocket.makefile('rw', 0) + self._write(file, 'hello jmpd %s'%(VERSION)) + + try: + while True: + line=self._read(file) + print line + except: + print_exc() + + def closeAll(self): + for socket in self._sockets: + socket.close() + +def _getName(line): + return line[0:line.find(': ')] +def _getValue(line): + return line[line.find(': ')+2:] + +tags=['title', 'artist', 'album', 'tracknumber', 'genre'] +class jmpd: + _lib=None # library + _settings=None + + def __init__(self): + self._lib=[] + self._settings=Settings("mpdconf") + self.readLib() + + def writeLib(self): + all_tags=tags + encoding=ENCODING + tags.append('filename') + + fout=codecs.open(self._settings.get("db_file"), mode='w', encoding=encoding) + fout.write("# jmpd\n") + fout.write("begin info\n") + fout.write("encoding: %s\n" % (encoding)) + fout.write("jmpd: %s\n" % (VERSION)) + fout.write("end info\n") + fout.write("\n") + + fout.write("begin library\n") + cur_path="" + for entry in self._lib: + if entry['path']!=cur_path: + fout.write("set path: %s\n" % entry['path']) + cur_path=entry['path'] + + fout.write("begin file\n") + for tag in all_tags: + if tag in entry: + fout.write("%s: %s\n" % (tag, entry[tag])) + fout.write("end file\n") + + fout.write("end library\n") + fout.close() + + def readLib(self): + """Restore the library from file""" + db_file=self._settings.get("db_file") + encoding=ENCODING + if os.path.exists(db_file)==False: + self._lib=[] + return + + # we first open the file in ASCII-mode, and read on till the real encoding + # then we reopen it in that encoding + fin=open(db_file, 'r') + fin.close() + fin=codecs.open(db_file, mode='r', encoding=encoding) + + self._readLine(fin) # begin info + encoding=_getValue(self._readLine(fin)) + version=_getValue(self._readLine(fin)) + if version!=VERSION: + raise Exception("Invalid database file") + self._readNext(fin, 'end info') + + self._readNext(fin, 'begin library') + cur_path="" + while True: + line=self._readLine(fin) + if line=="end library": + break + if _getName(line)=="set path: ": + cur_path=_getValue(line) + elif line=="begin file": + self._readFile(fin, cur_path, encoding) + + fin.close() + + def _readFile(self, fin, cur_path, encoding): + """Read the data for a file""" + entry={'filepath': cur_path} + line="" + while line!="end file": + line=self._readLine(fin) + if line.find(': ')>0: + entry[_getName(line)]=_getValue(line) + + self._lib.append(entry) + def _readEnd(self, fin, section): + """Read the next line containing an end section""" + line="" + while line!="end %s"%(section): + line=self._readLine(fin) + return line + def _readBegin(self, fin, section): + """Read the next line containing a begin section""" + line="" + while line!="begin %s"%(section): + line=self._readLine(fin) + return line + + def _readNext(self, fin, find): + line="" + while line!=find: + line=self._readLine(fin) + return line + def _readLine(self, fin): + """Reads the next line having characters and not being a commented line""" + ret="\n" + try: + while ret=="\n" or ret[0]=='#': + ret=fin.readline() + except: + pass + if len(ret)==0: + raise Exception("EOF") + return ret.strip() + + + def update(self): + """Look for songs in the music_directory""" + self._lib=[] + for dir in self._settings.get(MUSIC_DIRECTORY).split(":"): + self._update_rec(dir) + + self.writeLib() + + def _update_rec(self, path): + subdirs=[] + for filename in os.listdir(path): + filepath="%s/%s"%(path,filename) + if os.path.isdir(filepath): + subdirs.append(filepath) + if filename[-4:]==".mp3": + audio=EasyID3(filepath) + entry = {'path': path.decode(ENCODING) + , 'filename': filename.decode(ENCODING) + } + for tag in tags: + try: + value=str(audio[tag]) + entry[tag]=value[3:-2] + except: + entry[tag]='' + self._lib.append(entry) + + for subdir in subdirs: + self._update_rec(subdir) + +from test import port +s=server() +s.start(port) + @@ -1,4 +1,8 @@ from PyQt4 import QtCore, QtGui +import re +import urllib2, httplib, cookielib +import socket +socket.setdefaulttimeout(8) appIcon=QtGui.QIcon('gfx/icon.png') @@ -14,11 +18,112 @@ def sec2min(secs): if sec<10:sec='0'+str(sec) return str(min)+':'+str(sec) +def numeric_compare(x, y): + if x>y: + return 1 + elif x==y: + return 0 + return -1 def unique(seq): """Retrieve list of unique elements.""" seen = [] return t(c for c in seq if not (c in seen or seen.append(c))) +def format(string, song=None, xtra_tags={}): + """Replace all tags in $str with their respective value.""" + # what tags are available? + if song: + tags=song._data + else: + tags={} + for tag in xtra_tags: + tags[tag]=xtra_tags[tag] + + ret=string + # first perform some functions: $name(value) + func=re.compile('\$[a-z]{2}\(', re.MULTILINE|re.IGNORECASE) + start=0 + loops=20 # my stupidity is endless, so we better make sure to never always loop! + while True and loops>0: + loops-=1 + match=func.search(ret[start:]) + if not match: + break + # we've found a match! + # look for matching parenthesis! + start+=match.start() # we have to add, because we start search from $start! + end=ret.find('(', start)+1 + balance=1 + while balance>0 and end<len(ret): + if ret[end]==')':balance-=1 + if ret[end]=='(':balance+=1 + end+=1 # can do better + # whole function is in ret[start:end] + # find function-name + name=ret[start+1:ret.find('(', start)] + result=None # result of this function + if name=='if': + # $if(param,if-true) + comma=ret.find(',',start) + param1=ret[start+len('$if('):comma] + param2=ret[comma+1:end-1] + result='' + if param1[1:] in tags: + result=param2 + else: + start+=1 + if result!=None: + ret=("%s%s%s")%(ret[0:start], result, ret[end:]) + + # perform all replacements! + for tag in tags: + ret=ret.replace('$%s'%(tag), str(tags[tag])) + return ret + +def fetch(SE, sites, song=None, xtra_tags={}): + """Returns None when nothing found, or [site,source-url].""" + url=format(SE, song, xtra_tags) + url=url.replace(' ', '+') + + request=urllib2.Request(url) + request.add_header('User-Agent', 'montypc') + opener=urllib2.build_opener() + data=opener.open(request).read() + + # look for urls on the search page! + regex=re.compile('<a href="(.*?)".*?>.*?<\/a>') + urls=regex.findall(data) + + # look for predefined urls, which are good lyrics-sites + # we assume they are in order of importance; the first one matching + # is taken + finalURL=None + finalRegex=None + for url in urls: + if finalURL: + break + for site in sites: + if url.find(site)>=0: + finalURL=url + finalRegex=sites[site] + break + + match=None + if finalURL: + cj = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) + r = opener.open(finalURL) + data=r.read() + regex=re.compile(finalRegex, re.IGNORECASE|re.MULTILINE|re.DOTALL) + match=regex.search(data) + if match: + data=match.group(1) + data=data.replace('<br>', '<br />') + data=data.replace('<br />', '<br />') + data=data.strip() + return [data,finalURL] + return None + class Button(QtGui.QPushButton): iconSize=32 """A simple Button class which calls $onClick when clicked.""" @@ -8,19 +8,11 @@ from winMain import winMain from winSettings import winSettings from clMonty import monty from traceback import print_exc -#import hotshot, hotshot.stats try: wMain = winMain() wMain.show() app.exec_() - #prof=hotshot.Profile("profile") - #prof.runcall(app.exec_) - #prof.close() - #stats=hotshot.stats.load("profile") - #stats.strip_dirs() - #stats.sort_stats('time','calls') - #stats.print_stats(20) except Exception, e: print_exc() @@ -0,0 +1,3 @@ +music_directory /media/maxtor/music/library/Alberto Alcalá +#music_directory /media/maxtor/music/new/Isaac Hayes:/media/maxtor/music/new/Mule Variations +db_file db.txt diff --git a/plugins/AlbumCover.py b/plugins/AlbumCover.py index e30d1e6..512302e 100644 --- a/plugins/AlbumCover.py +++ b/plugins/AlbumCover.py @@ -19,21 +19,27 @@ AC_DEFAULT_FILES='cover,album' class wgAlbumCover(QtGui.QWidget): " container for the image" img=None + imgLoaded=False def __init__(self,parent=None): QtGui.QWidget.__init__(self,parent) + self.img=QtGui.QImage() monty.addListener('onSongChange', self.onSongChange) monty.addListener('onReady', self.onReady) monty.addListener('onDisconnect', self.onDisconnect) + monty.addListener('onStateChange', self.onSongChange) self.setMinimumSize(64,64) def mousePressEvent(self, event): self.onSongChange(None) + def getIMG(self): + if self.imgLoaded: + return self.img + return None + def paintEvent(self, event): - if not self.img: - return l=min(self.width(),self.height()) p=QtGui.QPainter(self) rect=QtCore.QRectF((self.width()-l)/2,0,l,l) @@ -51,6 +57,7 @@ class wgAlbumCover(QtGui.QWidget): def fetchCover(self, song): # set default cover, in case all else fails! + self.imgLoaded=False self.img.load('gfx/no-cd-cover.png') for i in xrange(3): src=settings.get('albumcover.fetch%i'%(i), str(i)) @@ -60,6 +67,7 @@ class wgAlbumCover(QtGui.QWidget): # (download and) load in self.img from a source! if self.fetchCoverSrc(song, src): # ACK! + self.imgLoaded=True break self.update() @@ -141,13 +149,11 @@ class wgAlbumCover(QtGui.QWidget): return False def onReady(self, params): - self.img=QtGui.QImage() - self.onSongChange(None) self.update() def onDisconnect(self, params): - self.img=None + self.img.load('') self.update() @@ -155,6 +161,7 @@ class pluginAlbumCover(Plugin): o=None def __init__(self, winMain): Plugin.__init__(self, winMain, 'AlbumCover') + def _load(self): self.o=wgAlbumCover(None) def getInfo(self): return "Display the album cover of the currently playing album." @@ -171,6 +178,9 @@ class pluginAlbumCover(Plugin): " albumcover.files: comma separated list of filenames (without extension)to be considered an album cover. The extensions from albumcover.gfx.ext are used.\n" \ " albumcover.gfx.ext: comma separated list of gfx-file extensions.\n" + def getWidget(self): + return self.o + def _getDockWidget(self): return self._createDock(self.o) diff --git a/plugins/Library.py b/plugins/Library.py index 4f82c2e..c4e9dc7 100644 --- a/plugins/Library.py +++ b/plugins/Library.py @@ -4,15 +4,15 @@ from clPlugin import * from misc import * from wgPlaylist import Playlist from wgSongList import clrRowSel +from clSettings import settings class pluginLibrary(Plugin): o=None def __init__(self, winMain): Plugin.__init__(self, winMain, 'Library') - self.o=Playlist(winMain, self, ['song'], 'The Library' + def _load(self): + self.o=Playlist(self.winMain, self, ['song'], 'Library' , self.onDoubleClick, self.onKeyPress) - self.o.showColumn(0,False) - self.o.setMode('library', 'artist/album') def getInfo(self): return "List showing all the songs allowing filtering and grouping." diff --git a/plugins/Lyrics.py b/plugins/Lyrics.py index 895a401..9cb0b4e 100644 --- a/plugins/Lyrics.py +++ b/plugins/Lyrics.py @@ -1,8 +1,5 @@ from PyQt4 import QtGui,QtCore -import urllib2, httplib, cookielib -import socket -socket.setdefaulttimeout(8) import re from thread import start_new_thread from traceback import print_exc @@ -104,64 +101,31 @@ class wgLyrics(QtGui.QWidget): if line.strip(): sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip() # construct URL to search! - url=settings.get('lyrics.engine', LY_DEFAULT_ENGINE) - url=url.replace('$artist', song.getArtist()) - url=url.replace('$title', song.getTitle()).replace('$album', song.getAlbum()) - url=url.replace(' ', '+') + SE=settings.get('lyrics.engine', LY_DEFAULT_ENGINE) try: - request=urllib2.Request(url) - request.add_header('User-Agent', 'montypc') - opener=urllib2.build_opener() - data=opener.open(request).read() - - # look for urls! - regex=re.compile('<a href="(.*?)".*?>.*?<\/a>') - urls=regex.findall(data) - - # look for predefined urls, which are good lyrics-sites - finalURL=None - finalRegex=None - for url in urls: - if finalURL: - break - for site in sites: - if url.find(site)>=0: - finalURL=url - finalRegex=sites[site] - break - - match=None - QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - #print finalURL - if finalURL: - cj = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) - r = opener.open(finalURL) - data=r.read() - regex=re.compile(finalRegex, re.IGNORECASE|re.MULTILINE|re.DOTALL) - match=regex.search(data) - #print match - if match: - data=match.group(1) - data=data.replace('<br>', '<br />') - data=data.replace('<br />', '<br />') - data=data.strip() + ret=fetch(SE, sites, song, {}) + if ret: + txt='%s<br /><br /><a href="%s">%s</a>'%(ret[0],ret[1],ret[1]) # save for later use! if lyFName: # we can't save if the path isn't correct - file=open(lyFName, 'w') - file.write(data) - file.close() - data='%s<br /><br />(source: <a href="%s">%s</a>)'%(data,finalURL,finalURL) + try: + file=open(lyFName, 'w') + file.write(ret[0]) + file.close() + except: + # probably a wrong path! + pass else: - data='Lyrics not found :\'(' - - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(data)) + txt="No lyrics found :'(" + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(txt)) except: print_exc() QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Oh noes, an error occured while fetching lyrics!')) + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, site unavailable!'\ + '<br />You have an internet connection?')) self._fetchCnt=0 def onDisconnect(self, params): @@ -178,6 +142,7 @@ class pluginLyrics(Plugin): o=None def __init__(self, winMain): Plugin.__init__(self, winMain, 'Lyrics') + def _load(self): self.o=wgLyrics(None) def getInfo(self): return "Show (and fetch) the lyrics of the currently playing song." diff --git a/plugins/Notify.py b/plugins/Notify.py new file mode 100644 index 0000000..d995d1c --- /dev/null +++ b/plugins/Notify.py @@ -0,0 +1,145 @@ +from PyQt4 import QtGui,QtCore +from thread import start_new_thread +from traceback import print_exc + +from misc import * +from clSettings import settings,mpdSettings +from clMonty import monty +from clPlugin import * + +class winNotify(QtGui.QWidget): + _timerID=None + resizeWindow=True + winMain=None + + # data used for showing off + timer=None + msg=None + song=None + xtra_tags=None + + def __init__(self, winMain, parent=None): + QtGui.QWidget.__init__(self, parent) + self.winMain=winMain + + self.setWindowFlags(QtCore.Qt.ToolTip) + self.setWindowOpacity(0.7) + + font=QtGui.QFont() + font.setPixelSize(30) + font.setFamily('Comic Sans Ms') + self.setFont(font) + + monty.addListener('onSongChange', self.onSongChange) + monty.addListener('onReady', self.onReady) + monty.addListener('onDisconnect', self.onDisconnect) + monty.addListener('onStateChange', self.onStateChange) + monty.addListener('onVolumeChange', self.onVolumeChange) + + def onSongChange(self, params): + self.show('$if($artist,$artist$if($album, [$album #$track])\n$title ($length))', monty.getCurrentSong()) + + def onReady(self, params): + self.show('montypc loaded!') + + def onDisconnect(self, params): + self.show('Disconnected!') + + def onStateChange(self, params): + self.show(params['newState']) + + def onVolumeChange(self, params): + self.show('Volume: %i%%'%(params['newVolume'])) + + def mousePressEvent(self, event): + self.hide() + + def show(self, msg, song=None, xtra_tags={}): + if not self.isVisible(): + self.setVisible(True) + self.resizeWindow=True + self.msg=msg + self.song=song + self.xtra_tags=xtra_tags + if self._timerID: + self.killTimer(self._timerID) + self._timerID=self.startTimer(200) + self.timer=11 + self.raise_() + self.timerEvent(None) + + def hide(self): + if self._timerID: + self.killTimer(self._timerID) + self._timerID=None + self.setHidden(True) + + + def centerH(self): + screen = QtGui.QDesktopWidget().screenGeometry() + size = self.geometry() + self.move((screen.width()-size.width())/2, 100) + def paintEvent(self, event): + p=QtGui.QPainter(self) + margin=3 # margin in pixels + spacing=10 # space between album cover and text + + # determine the Rect our message must fit in + txt="%s"%(format(self.msg, self.song, self.xtra_tags)) + rect=p.boundingRect(0,0,1,1, QtCore.Qt.AlignHCenter, txt) + + # check if 1/ albumcover plugin is loaded, and 2/ there is an + # album cover + width=0 + try: + cover=self.winMain.getPlugin('albumcover') + img=cover.getWidget().getIMG() + if img: + width=128 + else: + spacing=0 + except: + img=None + pass + + # do we have to resize? + if self.resizeWindow: + self.resizeWindow=False + self.resize(rect.width()+width+margin*3+spacing, max(width,rect.height())+margin*2) + self.centerH() + + # fill up with a nice color :) + p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(QtGui.QColor(230,230,255))) + + # draw album cover if necessary + if img: + rImg=QtCore.QRectF(margin,margin,width,width) + p.drawImage(rImg, img) + + # Pen In Black ... + p.setPen(QtCore.Qt.black) + rect=p.boundingRect(width+margin+spacing,margin, + rect.width(),self.height(), QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter, txt) + p.drawText(rect, QtCore.Qt.AlignHCenter, txt) + + def timerEvent(self, event): + self.timer-=1 + if self.timer<=0: + self.hide() + self.update() + + + +class pluginNotify(Plugin): + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Notify') + def _load(self): + self.o=winNotify(self.winMain) + def getInfo(self): + return "Show interesting events in a popup window." + + def _getSettings(self): + return [] + def afterSaveSettings(self): + self.o.onSongChange(None) diff --git a/plugins/PlayControl.py b/plugins/PlayControl.py index 06952e6..abe4547 100644 --- a/plugins/PlayControl.py +++ b/plugins/PlayControl.py @@ -3,6 +3,22 @@ from PyQt4 import QtGui, QtSvg, QtCore from misc import * from clMonty import monty from clPlugin import * +from thread import start_new_thread +from random import randint + +# Some predefined constants. +# Note that REPEAT precedes RANDOM. E.g. if repeat +# is ALBUM, and random is SONG, then it'll take a random song +# from the current album ... +PC_RANDOM_NO=0 # no randomness +PC_RANDOM_SONG=1 # choose a song random from the playlist +PC_RANDOM_ALBUM=2 # choose a random album, and play that fully + +PC_REPEAT_NO=0 # no repeat +PC_REPEAT_SONG=1 # repeat current song +PC_REPEAT_ALBUM=2 # repeat current album +PC_REPEAT_PLAYLIST=3 # repeat playlist + class wgPlayControl(QtGui.QWidget): """Displays controls for interacting with playing, like play, volume ...""" @@ -20,6 +36,12 @@ class wgPlayControl(QtGui.QWidget): " all objects in this widget" objects=None + cmbRepeat=None + cmbShuffle=None + + " contains the songs of the album the current song is playing. None, if the album is not set" + curAlbumSongs=None + def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) @@ -56,11 +78,28 @@ class wgPlayControl(QtGui.QWidget): self.btnPrevious=Button("prev", self.onBtnPreviousClick, 'gfx/media-skip-backward.svg', True) self.btnNext=Button("next", self.onBtnNextClick, 'gfx/media-skip-forward.svg', True) + self.cmbShuffle=QtGui.QComboBox(self) + self.cmbShuffle.addItem('Don\'t play dices') + self.cmbShuffle.addItem('Random song') + self.cmbShuffle.addItem('Random album') + self.cmbShuffle.setCurrentIndex(int(settings.get('playcontrol.shuffle', PC_RANDOM_SONG))) + + self.cmbRepeat=QtGui.QComboBox(self) + self.cmbRepeat.addItem('No repeat') + self.cmbRepeat.addItem('Repeat current song') + self.cmbRepeat.addItem('Repeat album') + self.cmbRepeat.addItem('Playlist') + self.cmbRepeat.setCurrentIndex(int(settings.get('playcontrol.repeat', PC_REPEAT_PLAYLIST))) + self.objects=[self.slrVolume, self.slrTime, self.btnStop, self.btnNext, self.btnPrevious] layout=QtGui.QHBoxLayout(parent) - self.setLayout(layout) + layout2=QtGui.QHBoxLayout(parent) + layoutWidget=QtGui.QVBoxLayout(parent) + layoutWidget.addLayout(layout) + layoutWidget.addLayout(layout2) + self.setLayout(layoutWidget) layout.addWidget(self.btnPrevious) layout.addWidget(self.btnPlayPause) @@ -69,12 +108,18 @@ class wgPlayControl(QtGui.QWidget): layout.addWidget(self.slrTime) layout.addWidget(self.slrVolume) layout.addWidget(self.svgVolume) + + layout2.addWidget(self.cmbRepeat) + layout2.addWidget(self.cmbShuffle) self.connect(self.slrVolume, QtCore.SIGNAL('valueChanged(int)'),self.onVolumeSliderChange) self.connect(self.slrTime, QtCore.SIGNAL('sliderReleased()'),self.onTimeSliderChange) + self.connect(self.cmbRepeat, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbRepeatChanged) + self.connect(self.cmbShuffle, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbShuffleChanged) + monty.addListener('onStateChange', self.onStateChange) - monty.addListener('onSongChange', self.onSongChange) + monty.addListener('beforeSongChange', self.beforeSongChange) monty.addListener('onSongChange', self.onSongChange) monty.addListener('onVolumeChange', self.onVolumeChange) monty.addListener('onReady', self.onStateChange) @@ -105,7 +150,68 @@ class wgPlayControl(QtGui.QWidget): self.slrTime.setEnabled(True) except: pass + # look in another thread for the songs in the current album! + params=() + start_new_thread(self.findAlbumSongs, params) + def beforeSongChange(self, params): + nextID=None + song=monty.getCurrentSong() + # decide here what next song to play! + repeat=self.cmbRepeat.currentIndex() + random=self.cmbShuffle.currentIndex() + # is the current song the last of the album? + eofAlbum=int(song.getTrack())==int(self.curAlbumSongs[-1].getTrack()) + if repeat==PC_REPEAT_NO: + # no repeat, nothing to see here! Pass on! + pass + elif repeat==PC_REPEAT_SONG: + # we must repeat the previous song! + nextID=params['curSongID'] + elif repeat==PC_REPEAT_ALBUM: + # check if we are at the last track, if it is, we must start a new! + if eofAlbum: + nextID=self.curAlbumSongs[0].getID() + elif repeat==PC_REPEAT_PLAYLIST: + # repeating the playlist is handled by monty itself; + # it is set in onCmbRepeatChanged. + pass + + if random==PC_RANDOM_NO: + # just follow our leader Monty. + pass + elif random==PC_RANDOM_SONG: + # pick a random song! This depends on what repeat-mode we're in. + if repeat==PC_REPEAT_NO or repeat==PC_REPEAT_PLAYLIST: + # we don't repeat anything, so we can just let monty pick the + # next random one! + pass + elif repeat==PC_REPEAT_SONG: + # song repeat; don't choose the next one! + nextID=params['curSongID'] + elif repeat==PC_REPEAT_ALBUM and self.curAlbumSongs: + # pick random song from current album + nextID=self.curAlbumSongs[randint(0,len(self.curAlbumSongs)-1)].getID() + elif random==PC_RANDOM_ALBUM: + # pick a random album! This means, we pick the first song of a random + # album. + if eofAlbum and (repeat==PC_REPEAT_PLAYLIST or repeat==PC_REPEAT_NO): + # all first songs of an album + albums=filter(lambda s: s.getAlbum() and s.getTrack()==1, monty.listPlaylist()) + nextID=albums[randint(0,len(albums)-1)].getID() + + if nextID!=None: + monty.play(nextID) + + def findAlbumSongs(self): + """This method looks for the songs in the album of current playing song.""" + song=monty.getCurrentSong() + self.curAlbumSongs=None + if not song or not song.getAlbum(): + return + self.curAlbumSongs=filter(lambda s: s.getArtist()==song.getArtist() + and s.getAlbum()==song.getAlbum(), monty.listPlaylist()) + self.curAlbumSongs.sort(numeric_compare) def onBtnPlayPauseClick(self): status=monty.getStatus() @@ -135,13 +241,26 @@ class wgPlayControl(QtGui.QWidget): else: mode=('0', 'min', 'med', 'max')[int(3*v/100)] self.svgVolume.load('gfx/stock_volume-%s.svg'%(mode)) - + + def onCmbRepeatChanged(self, newval): + settings.set('playcontrol.repeat', newval) + if newval==PC_REPEAT_PLAYLIST: + monty.repeat(1) + else: + monty.repeat(0) + def onCmbShuffleChanged(self, newval): + settings.set('playcontrol.shuffle', newval) + if newval==PC_RANDOM_SONG: + monty.random(1) + else: + monty.random(0) class pluginPlayControl(Plugin): o=None def __init__(self, winMain): Plugin.__init__(self, winMain, 'PlayControl') + def _load(self): self.o=wgPlayControl(None) def getInfo(self): return "Have total control over the playing!" diff --git a/plugins/Playlist.py b/plugins/Playlist.py index 1a13545..81c92ad 100644 --- a/plugins/Playlist.py +++ b/plugins/Playlist.py @@ -4,14 +4,15 @@ from clPlugin import * from misc import * from wgPlaylist import Playlist from wgSongList import clrRowSel +from clSettings import settings class pluginPlaylist(Plugin): o=None def __init__(self, winMain): Plugin.__init__(self, winMain, 'Playlist') - self.o=Playlist(winMain, self, ['artist', 'title', 'album'], 'The Playlist' + def _load(self): + self.o=Playlist(self.winMain, self, ['artist', 'title', 'track', 'album'], 'Playlist' , self.onDoubleClick, self.onKeyPress) - self.o.setMode('playlist') monty.addListener('onSongChange', self.onSongChange) def getInfo(self): return "The playlist showing the songs that will be played." diff --git a/plugins/Shortcuts.py b/plugins/Shortcuts.py new file mode 100644 index 0000000..f8956f5 --- /dev/null +++ b/plugins/Shortcuts.py @@ -0,0 +1,48 @@ +from PyQt4 import QtGui, QtCore + +from Xlib.display import Display +from Xlib import X +from Xlib.XK import * +from thread import start_new_thread + +from clPlugin import Plugin +from clSettings import settings + + +class pluginShortcuts(Plugin): + display=None + root=None + def __init__(self, winMain): + global keycodes + Plugin.__init__(self, winMain, 'Shortcuts') + + def _load(self): + return + self.disp=Display() + self.root=self.disp.screen().root + self.root.change_attributes(event_mask=X.KeyPressMask) + #for keycode,name in enumerate(keycodes): + #print keycode + #self.root.grab_key(keycode, X.AnyModifier, 0, X.GrabModeAsync, X.GrabModeAsync) + start_new_thread(self.keyHandler, (None,)) + + def err(self,param=None,param2=None): + print param + print param2 + + def getInfo(self): + return "Control playing using shortcuts." + + def _getSettings(self): + return [] + return [ + ['shortcuts.playpauze', 'Play/pauze', 'Key to start playing/pauze.', QtGui.QLineEdit(settings.get('shortcuts.playpauze', 'Win+Home'))], + ] + def afterSaveSettings(self): + pass + + def keyHandler(self, emptyParam): + while 1: + event=self.root.display.next_event() + keycode=event.detail + print keycode diff --git a/plugins/SongStatus.py b/plugins/SongStatus.py index 65e1866..a03476e 100644 --- a/plugins/SongStatus.py +++ b/plugins/SongStatus.py @@ -4,10 +4,10 @@ from clPlugin import * from traceback import print_exc SS_DEFAULT_FORMAT='<font size="4">now $state</font>'\ -'<font size="8" color="blue">$title</font>'\ +'$if($title,<font size="8" color="blue">$title</font>'\ '<br />by <font size="8" color="green">$artist</font>'\ -'<br /><font size="5" color="red">[$album # $track]</font>'\ -'<br /><font size="4">$time/$length</font>' +'<br /><font size="5" color="red">[$album # $track]</font>)'\ +'$if($length,<br /><font size="4">$time/$length</font>)' class wgSongStatus(QtGui.QWidget): """Displays the status of the current song, if playing.""" @@ -34,21 +34,16 @@ class wgSongStatus(QtGui.QWidget): status=monty.getStatus() song=monty.getCurrentSong() - values={'state':'', 'time':'', 'length':'', 'title':'', 'artist':'', 'album':'', 'track':''} + values={'state':''} try: values['state']={'play':'playing', 'stop':'stopped', 'pause':'paused'}[status['state']] - values['time']=sec2min(status['time']) - values['length']=sec2min(status['length']) - values['title']=song.getTitle() - values['artist']=song.getArtist() - values['album']=song.getAlbum() - values['track']=song.getTrack() + if 'time' in status: + values['length']=sec2min(status['length']) + values['time']=sec2min(status['time']) except: pass - txt=settings.get('songstatus.format', SS_DEFAULT_FORMAT) - for key in values: - txt=txt.replace('$%s'%(key), values[key]) + txt=format(settings.get('songstatus.format', SS_DEFAULT_FORMAT), song, values) self.lblInfo.setText(txt) def text(self): @@ -58,6 +53,7 @@ class pluginSongStatus(Plugin): o=None def __init__(self, winMain): Plugin.__init__(self, winMain, 'SongStatus') + def _load(self): self.o=wgSongStatus(None) def getInfo(self): return "Show information about the current song." diff --git a/plugins/Systray.py b/plugins/Systray.py index 5f0f38f..3352585 100644 --- a/plugins/Systray.py +++ b/plugins/Systray.py @@ -8,10 +8,13 @@ class pluginSystray(Plugin): def __init__(self, winMain): Plugin.__init__(self, winMain, 'Systray') - self.o=QtGui.QSystemTrayIcon(appIcon, winMain) + def _load(self): + self.o=QtGui.QSystemTrayIcon(appIcon, self.winMain) self.o.show() - winMain.connect(self.o, QtCore.SIGNAL('activated (QSystemTrayIcon::ActivationReason)') + self.winMain.connect(self.o, QtCore.SIGNAL('activated (QSystemTrayIcon::ActivationReason)') , self.onSysTrayClick) + def _unload(self): + self.o.hide() def getInfo(self): return "Display the montypc icon in the systray." diff --git a/plugins/Tabs.py b/plugins/Tabs.py new file mode 100644 index 0000000..12ae0e7 --- /dev/null +++ b/plugins/Tabs.py @@ -0,0 +1,161 @@ +from PyQt4 import QtGui,QtCore + +import re +from thread import start_new_thread +from traceback import print_exc + +from misc import * +from clSettings import settings,mpdSettings +from clMonty import monty +from clPlugin import * + +class ResetEvent(QtCore.QEvent): + song=None + def __init__(self, song=None): + QtCore.QEvent.__init__(self,QtCore.QEvent.User) + self.song=song +class AddHtmlEvent(QtCore.QEvent): + html=None + def __init__(self,html): + QtCore.QEvent.__init__(self,QtCore.QEvent.User) + self.html=html + +TABS_DEFAULT_ENGINE='http://www.google.com/search?q=tabs|chords+"$artist"+"$title"' +TABS_DEFAULT_SITES='azchords.com <pre>(.*?)</pre>\n'\ + 'fretplay.com <P CLASS="tabs">(.*?)</P>\n'\ + 'guitaretab.com <pre style="COLOR: #000000; FONT-SIZE: 11px;">(.*)?</pre>'\ + +class wgTabs(QtGui.QWidget): + " contains the tabs" + txt=None + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + self.txt=QtGui.QTextEdit(parent) + self.txt.setReadOnly(True) + + layout=QtGui.QVBoxLayout() + layout.addWidget(self.txt) + self.setLayout(layout) + + monty.addListener('onSongChange', self.onSongChange) + monty.addListener('onReady', self.onReady) + monty.addListener('onDisconnect', self.onDisconnect) + + def onSongChange(self, params): + song=monty.getCurrentSong() + try: + song._data['file'] + except: + self.resetTxt() + return + + self.resetTxt(song) + start_new_thread(self.fetchTabs, (song,)) + + def customEvent(self, event): + if isinstance(event,ResetEvent): + self.resetTxt(event.song) + elif isinstance(event,AddHtmlEvent): + self.txt.insertHtml(event.html) + + def onReady(self, params): + self.onSongChange(None) + + _mutex=QtCore.QMutex() + _fetchCnt=0 + def fetchTabs(self, song): + # only allow 1 instance to look tabs! + self._mutex.lock() + if self._fetchCnt: + self._mutex.unlock() + return + self._fetchCnt=1 + self._mutex.unlock() + + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + + # save the data to file! + save_dir=settings.get('tabs.dir', '/holy_grail') + fInfo=QtCore.QFileInfo(save_dir) + if fInfo.isDir(): + tabsFName='%s/%s - %s.txt'%(save_dir,song.getArtist(),song.getTitle()) + else: + tabsFName=None + # does the file exist? if yes, read that one! + try: + # we have it: load, and return! + file=open(tabsFName, 'r') + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(file.read())) + file.close() + self._fetchCnt=0 + return + except: + pass + + # fetch from inet + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<i>Searching tabs ...</i>')) + + lines=settings.get('tabs.sites', TABS_DEFAULT_SITES).split('\n') + sites={} + for line in lines: + if line.strip(): + sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip() + # construct URL to search! + SE=settings.get('tabs.engine', TABS_DEFAULT_ENGINE) + try: + ret=fetch(SE, sites, song, {}) + if ret: + txt='<pre>%s<br /><br /><a href="%s">%s</a></pre>'%(ret[0],ret[1],ret[1]) + # save for later use! + if tabsFName: + # we can't save if the path isn't correct + try: + file=open(tabsFName, 'w') + file.write(ret[0]) + file.close() + except: + pass + else: + txt="No tabs found :'(" + + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(txt)) + except: + print_exc() + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, site unavailable!'\ + '<br />You have an internet connection?')) + self._fetchCnt=0 + + def onDisconnect(self, params): + self.resetTxt() + + def resetTxt(self, song=None): + self.txt.clear() + if song: + self.txt.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\ + '<br />\n\n'%(song.getTitle(), song.getArtist())) + + +class pluginTabs(Plugin): + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Tabs') + def _load(self): + self.o=wgTabs(None) + def getInfo(self): + return "Show (and fetch) the tabs of the currently playing song." + + def _getDockWidget(self): + return self._createDock(self.o) + + def _getSettings(self): + sites=QtGui.QTextEdit() + sites.insertPlainText(settings.get('tabs.sites', TABS_DEFAULT_SITES)) + return [ + ['tabs.engine', 'Search engine', 'The URL that is used to search. $artist, $title and $album are replaced in the URL.', QtGui.QLineEdit(settings.get('tabs.engine', TABS_DEFAULT_ENGINE))], + ['tabs.sites', 'Sites & regexes', 'This field contains all sites, together with the regex needed to fetch the tabs.\nEvery line must look like this: $domain $regex-start(.*?)$regex-end\n$domain is the domain of the tabs website, $regex-start is the regex indicating the start of the tabs, $regex-end indicates the end. E.g. footabs.org <tabs>(.*?)</tabs>', sites], + ['tabs.dir', 'Tabs directory', 'Directory where tabs should be stored and retrieved.', QtGui.QLineEdit(settings.get('tabs.dir'))], + ] + def afterSaveSettings(self): + self.o.onSongChange(None) diff --git a/plugins/__init__.py b/plugins/__init__.py index c0db019..d83b441 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -9,5 +9,13 @@ for file in os.listdir('plugins'): name=file[:-3] # name without ext pkg='plugins.%s'%(name) # pkg name pluginName='plugin%s'%(name) # classname - __import__(pkg, globals(), locals(), pluginName, -1) - _plugins.append([pkg, pluginName]) + + module=__import__(pkg, globals(), locals(), pluginName, -1) + try: + # check here if the file contains an entity (let's hope + # a class!) with $pluginName! + module.__dict__[pluginName] + _plugins.append([pkg, pluginName]) + except: + #print "Failed to load plugin "+pluginName + pass diff --git a/amazon_cover_fetcher.py b/plugins/amazon_cover_fetcher.py index 2de0181..2de0181 100644 --- a/amazon_cover_fetcher.py +++ b/plugins/amazon_cover_fetcher.py @@ -0,0 +1,19 @@ +from thread import start_new_thread +from traceback import print_exc + +#import jmpd +#import jmpc + +port=6600 + +#server=jmpd.server() +#start_new_thread(server.start, (port,)) + +#try: + #client=jmpc.client() + #client.connect('localhost', port) +#except: + #print_exc() + +#server.closeAll() +#client.close() diff --git a/wgPlaylist.py b/wgPlaylist.py index 0b670e4..a6771c0 100644 --- a/wgPlaylist.py +++ b/wgPlaylist.py @@ -3,6 +3,7 @@ from PyQt4 import QtGui, QtCore from wgSongList import SongList from misc import * +from clSettings import settings class Playlist(QtGui.QWidget): @@ -33,7 +34,11 @@ class Playlist(QtGui.QWidget): self.cmbMode.addItem('genre') self.cmbMode.addItem('genre/artist') self.cmbMode.addItem('genre/artist/album') - self.lstSongs=SongList(parent, headers, onDoubleClick) + self.lstSongs=SongList(parent, self.name, headers, onDoubleClick) + + i=int(settings.get("l%s.mode"%(self.name), 0)) + self.cmbMode.setCurrentIndex(i) + self.onModeChangeChange(str(self.cmbMode.itemText(i))) frame=QtGui.QVBoxLayout() frame.addWidget(self.cmbMode) @@ -97,6 +102,7 @@ class Playlist(QtGui.QWidget): self.lstSongs.setMode('playlist') else: self.lstSongs.setMode('library', str(value)) + settings.set("l%s.mode"%(self.name), self.cmbMode.currentIndex()) self.filter() def showColumn(self, column, show=True): diff --git a/wgSongList.py b/wgSongList.py index bf9d197..cac8b2c 100644 --- a/wgSongList.py +++ b/wgSongList.py @@ -5,6 +5,7 @@ from traceback import print_exc from misc import * from clSong import Song +from clSettings import settings # constants used for fSongs LIB_ROW=0 @@ -79,13 +80,16 @@ class SongList(QtGui.QWidget): wgGfxArtist=QtSvg.QSvgRenderer('gfx/user_icon.svg') - def __init__(self, parent, headers, onDoubleClick): + def __init__(self, parent, name, 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.name=name + # load the headers, and fetch from the settings the width and visibility + self.headers=map(lambda h: [h, int(settings.get('l%s.%s.width'%(self.name,h),250)) + , settings.get('l%s.%s.visible'%(self.name,h),'1')=='1'], headers) + self.headers.insert(0, ['id', 30, settings.get('l%s.%s.visible'%(self.name,'id'),'0')=='1']) self.songs=None self.numSongs=None self.fSongs=None @@ -130,6 +134,9 @@ class SongList(QtGui.QWidget): def sizeHint(self): return QtCore.QSize(10000,10000) + def getSongs(self): + return self.songs + def setMode(self, mode, groupBy=''): self.selRows=[] self.selIDs=[] @@ -555,10 +562,13 @@ class SongList(QtGui.QWidget): self.update() elif self.resizeCol!=None: + # store this new width! + self.saveColumnWidth(self.resizeCol) # we're not resizing anymore! self.resizeCol=None self.update() - + def saveColumnWidth(self, col): + settings.set('l%s.%s.width'%(self.name,self.headers[col][0]), self.headers[col][1]) def mouseDoubleClickEvent(self, event): pos=event.pos() row=self._pos2row(pos) @@ -599,6 +609,7 @@ class SongList(QtGui.QWidget): 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.saveColumnWidth(self.resizeColumn) self.resizeColumn=None self.resizeEvent(None) if self.redrawID!=None: @@ -849,7 +860,7 @@ class SongList(QtGui.QWidget): p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3)) self._paintCnt=0 - + #text='%s - %s' % (self.selMiscs, '') #text='%s - %s' % (str(self.selRows), str(self.selIDs)) #r=QtCore.QRect(10,self.height()-40,self.width()-20,20) @@ -23,10 +23,12 @@ class winMain(QtGui.QMainWindow): """The winMain class is mpc's main window, showing the playlists and control-interface""" " list of plugins that are loaded. name=>Plugin" plugins={} + docks=[] " menus" mConnect=None mDisconnect=None + mLayout=None " connection window" wConnect=None @@ -40,19 +42,8 @@ class winMain(QtGui.QMainWindow): QtGui.QWidget.__init__(self, parent) self.setWindowTitle("montypc - An MPD client") - self.plugins={} - opts=QtGui.QDockWidget.DockWidgetClosable|QtGui.QDockWidget.DockWidgetMovable - for p in plugins.listPlugins(): - pkg,name=p[0],p[1] - # WARNING - dirty hack ahead! - plugin=eval('%s.%s'%(pkg,name))(self) - self.plugins[plugin.getName().lower()]=plugin - dock=plugin.getDockWidget(opts) - if dock: - self.addDockWidget(QtCore.Qt.TopDockWidgetArea, plugin.getDockWidget(opts)) - self.wConnect=winConnect() - + self.statusBar() # create a statusbar mBar=self.menuBar() # create a menubar # menu file @@ -76,16 +67,22 @@ class winMain(QtGui.QMainWindow): m.addAction("Settings", self.showWinSettings).setIcon(QtGui.QIcon('gfx/gtk-preferences.svg')) # menu layout - m=mBar.addMenu("Layout") - m.setTearOffEnabled(True) - m.addAction('Save layout', self.saveLayout) - m.addAction('Restore layout', self.restoreLayout) - m.addSeparator() - # can not use iterators, as that gives some creepy error 'bout c++ - actions=self.createPopupMenu().actions() - for i in xrange(len(actions)): - m.addAction(actions[i]) + self.mLayout=mBar.addMenu("Layout") + self.mLayout.setTearOffEnabled(True) + + self.plugins={} + newPlugins=False # are there new plugins? + for p in plugins.listPlugins(): + pkg,name=p[0],p[1] + # WARNING - dirty hack ahead! + plugin=eval('%s.%s'%(pkg,name))(self) + newPlugins=newPlugins|(settings.get('%s.load'%(plugin.getName(True)), None)==None) + self.plugins[plugin.getName().lower()]=plugin + if settings.get('%s.load'%(plugin.getName(True)), '1')=='1': + # load new plugins by default + plugin.load() + self.updateLayoutMenu() self.setDockOptions(QtGui.QMainWindow.AllowNestedDocks \ |QtGui.QMainWindow.AllowTabbedDocks \ |QtGui.QMainWindow.VerticalTabs) @@ -113,9 +110,55 @@ class winMain(QtGui.QMainWindow): self.wConnect.monitor() self.show() - #self.showWinSettings() + if newPlugins: + self.showWinSettings() doEvents() + def updateLayoutMenu(self): + self.mLayout.clear() + self.mLayout.addAction('Save layout', self.saveLayout) + self.mLayout.addAction('Restore layout', self.restoreLayout) + self.mLayout.addSeparator() + # create checkable menu + a=QtGui.QAction('Show titlebars', self) + a.setCheckable(True) + a.setChecked(settings.get('winMain.show.titlebars', '1')=='1') + self.toggleTitleBars(a.isChecked()) + self.connect(a, QtCore.SIGNAL('toggled(bool)'), self.toggleTitleBars) + self.mLayout.addAction(a) + self.mLayout.addSeparator() + # can not use iterators, as that gives some creepy error 'bout c++ + actions=self.createPopupMenu().actions() + for i in xrange(len(actions)): + self.mLayout.addAction(actions[i]) + + def toggleTitleBars(self, val): + if val: + settings.set('winMain.show.titlebars', '1') + else: + settings.set('winMain.show.titlebars', '0') + for dock in self.docks: + if val: + dock.setTitleBarWidget(None) + else: + dock.setTitleBarWidget(QtGui.QWidget()) + + def getPlugin(self, name): + try: + return self.plugins[name.lower()] + except: + return None + + def addDock(self, dock): + if dock: + self.docks.append(dock) + self.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock) + self.updateLayoutMenu() + def removeDock(self, dock): + if dock: + self.docks.remove(dock) + self.removeDockWidget(dock) + self.updateLayoutMenu() mMenuVisible=None def createPopupMenu(self): @@ -156,6 +199,7 @@ class winMain(QtGui.QMainWindow): if not self.wSettings: self.wSettings=winSettings(self) self.wSettings.show() + self.wSettings.raise_() def onReady(self, params): self.initialiseData() diff --git a/winSettings.py b/winSettings.py index 7951da0..310673f 100644 --- a/winSettings.py +++ b/winSettings.py @@ -2,11 +2,13 @@ from PyQt4 import QtGui, QtCore from misc import * from clMonty import monty +from clSettings import settings class winSettings(QtGui.QWidget): btnSave=None btnClose=None + lstPlugins=None winMain=None @@ -17,20 +19,28 @@ class winSettings(QtGui.QWidget): self.btnSave=Button('save all', self.onBtnSaveClick) self.btnClose=Button('close', self.onBtnCloseClick) - tabPlugins=QtGui.QTextEdit(self) - tabPlugins.setReadOnly(True) + self.lstPlugins=QtGui.QListWidget(self) layoutWin=QtGui.QVBoxLayout() layoutButtons=QtGui.QHBoxLayout() tabWidget=QtGui.QTabWidget(parent) self.setLayout(layoutWin) - tabWidget.addTab(tabPlugins, "plugins") + tabWidget.addTab(self.lstPlugins, "plugins") for p in self.winMain.plugins: plugin=self.winMain.plugins[p] - tabPlugins.insertHtml("%s: <i>%s</i><br />"%(plugin.getName(), plugin.getInfo())) + item=QtGui.QListWidgetItem("%s\t%s"%(plugin.getName(), plugin.getInfo())) + if plugin.isLoaded(): + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + + if settings.get('%s.load'%(plugin.getName(True)),None)==None: + # load new plugins by default + item.setTextColor(QtCore.Qt.blue) + settings.set('%s.load'%(plugin.getName(True)), 1) + self.lstPlugins.addItem(item) tabWidget.addTab(plugin.getSettingsWidget(), plugin.getName()) - layoutWin.addWidget(tabWidget) layoutWin.addLayout(layoutButtons) @@ -39,6 +49,8 @@ class winSettings(QtGui.QWidget): layoutButtons.addWidget(self.btnSave) layoutButtons.addWidget(self.btnClose) + self.connect(self.lstPlugins, QtCore.SIGNAL('itemChanged (QListWidgetItem*)'), self.onlstPluginItemChanged) + self.setWindowIcon(appIcon) self.setWindowTitle('Settings') self.setAttribute(QtCore.Qt.WA_DeleteOnClose) @@ -57,7 +69,17 @@ class winSettings(QtGui.QWidget): plugin.saveSettings() def onBtnCloseClick(self): self.close() - + def onlstPluginItemChanged(self, item): + # check here if we have to load or unload the plugin! + pluginName=str(item.text()[0:str(item.text()).find('\t')]) + plugin=self.winMain.getPlugin(pluginName) + loaded=int(item.checkState()==QtCore.Qt.Checked) + settings.set('%s.load'%(plugin.getName(True)), loaded) + if loaded: + plugin.load() + else: + plugin.unload() + return def closeEvent(self, event): for p in self.winMain.plugins: plugin=self.winMain.plugins[p] |