#! /usr/bin/env python # # Copyright (C) 2004, 2005 Richard Kettlewell # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA # # THIS PROGRAM IS NO LONGER MAINTAINED. # # It worked last time I tried running it, but all client maintenance # effort is now devoted to the web interface and the GTK+ client # (Disobedience). """Graphical user interface for DisOrder""" from Tkinter import * import tkFont import Queue import threading import disorder import time import string import re import getopt import sys ######################################################################## # Architecture: # # The main (initial) thread of the program runs all GUI code. The GUI is only # directly modified from inside this thread. We sometimes call this the # master thread. # # We have a background thread, MonitorStateThread, which waits for changes to # the server's state which we care about. Whenever such a change occurs it # notifies all the widgets which care about it (and possibly other widgets; # the current implementation is unsophisticated.) # # Widget poll() methods usually, but NOT ALWAYS, called in the # MonitorStateThread. Other widget methods are call in the master thread. # # We have a separate disorder.client for each thread rather than locking a # single client. MonitorStateThread also has a private disorder.client that # it uses to watch for server state changes. ######################################################################## class Intercom: # communication queue into thread containing Tk event loop # # Sets up a callback on the event loop (in this thread) which periodically # checks the queue for elements; if any are found they are executed. def __init__(self, master): self.q = Queue.Queue(); self.master = master self.poll() def poll(self): try: item = self.q.get_nowait() item() self.master.after_idle(self.poll) except Queue.Empty: self.master.after(100, self.poll) def put(self, item): self.q.put(item) ######################################################################## class ProgressBar(Canvas): # progress bar widget def __init__(self, master=None, **kw): Canvas.__init__(self, master, highlightthickness=0, **kw) self.outer = self.create_rectangle(0, 0, 0, 0, outline="#000000", width=1, fill="#ffffff") self.bar = self.create_rectangle(0, 0, 0, 0, width=1, fill="#ff0000", outline='#ff0000') self.current = None self.total = None self.bind("", lambda e: self.redisplay()) def update(self, current, total): self.current = current if current > total: current = total elif current < 0: current = 0 self.total = total self.redisplay() def clear(self): self.current = None self.total = None self.redisplay() def redisplay(self): w, h = self.winfo_width(), self.winfo_height() if w > 0 and h > 0: self.coords(self.outer, 0, 0, w - 1, h - 1) if self.total: bw = int((w - 2) * self.current / self.total) self.itemconfig(self.bar, fill="#ff0000", outline="#ff0000") self.coords(self.bar, 1, 1, bw, h - 2) else: self.itemconfig(self.bar, fill="#909090", outline="#909090") self.coords(self.bar, 1, 1, w - 2, h - 2) # look up a track's name part, using client c. Maintains a cache. part_cache = {} def part(c, track, context, part): key = "%s-%s-%s" % (part, context, track) now = time.time() if not part_cache.has_key(key) or part_cache[key]['when'] < now - 3600: part_cache[key] = {'when': now, 'what': c.part(track, context, part)} return part_cache[key]['what'] class PlayingWidget(Frame): # widget that always displays information about what's # playing def __init__(self, master=None, **kw): Frame.__init__(self, master, **kw) # column 0 is descriptions, column 1 is the values self.columnconfigure(0,weight=0) self.columnconfigure(1,weight=1) self.fields = {} self.field(0, 0, "artist", "Artist") self.field(1, 0, "album", "Album") self.field(2, 0, "title", "Title") # column 1 also has the progress bar in it self.p = ProgressBar(self, height=20) self.p.grid(row=3, column=1, sticky=E+W) # column 2 has operation buttons b = Button(self, text="Quit", command=self.quit) b.grid(row=0, column=2, sticky=E+W) b = Button(self, text="Scratch", command=self.scratch) b.grid(row=1, column=2, sticky=E+W) b = Button(self, text="Recent", command=self.recent) b.grid(row=2, column=2, sticky=E+W) self.length = 0 self.update_length() self.last = None self.recentw = None def field(self, row, column, name, label): # create a field Label(self, text=label).grid(row=row, column=column, sticky=E) self.fields[name] = Text(self, height=1, state=DISABLED) self.fields[name].grid(row=row, column=column + 1, sticky=W+E); def set(self, name, value): # set a field's value f = self.fields[name] f.config(state=NORMAL) f.delete(1.0, END) f.insert(END, value) f.config(state=DISABLED) def playing(self, p): # called with new what's-playing information values = {} if p: for tpart in ['artist', 'album', 'title']: values[tpart] = part(client, p['track'], 'display', tpart) try: self.length = client.length(p['track']) except disorder.operationError: self.length = 0 self.started = int(p['played']) else: self.length = 0 for k in self.fields.keys(): if k in values: self.set(k, values[k]) else: self.set(k, "") self.length_bar() def length_bar(self): if self.length and self.length > 0: self.p.update(time.time() - self.started, self.length) else: self.p.clear() def update_length(self): self.length_bar() self.after(1000, self.update_length) def poll(self, c): p = c.playing() if p != self.last: intercom.put(lambda: self.playing(p)) self.last = p def quit(self): sys.exit(0) def scratch(self): client.scratch() def recent_close(self): self.recentw.destroy() self.recentw = None def recent(self): if self.recentw: self.recentw.deiconify() self.recentw.lift() else: w = 80*tracklistFont.measure('A') h = 40*tracklistFont.metrics("linespace") self.recentw = Toplevel() self.recentw.protocol("WM_DELETE_WINDOW", self.recent_close) self.recentw.title("Recently Played") # XXX for some reason Toplevel(width=w,height=h) doesn't seem to work self.recentw.geometry("%dx%d" % (w,h)) w = RecentWidget(self.recentw) w.pack(fill=BOTH, expand=1) mst.add(w); class TrackListWidget(Frame): def __init__(self, master=None, **kw): Frame.__init__(self, master, **kw) self.yscrollbar = Scrollbar(self) self.xscrollbar = Scrollbar(self, orient=HORIZONTAL) self.canvas = Canvas(self, xscrollcommand=self.xscrollbar.set, yscrollcommand=self.yscrollbar.set) self.xscrollbar.config(command=self.canvas.xview) self.yscrollbar.config(command=self.canvas.yview) self.canvas.grid(row=0, column=0, sticky=N+S+E+W) self.yscrollbar.grid(row=0, column=1, sticky=N+S) self.xscrollbar.grid(row=1, column=0, sticky=E+W) self.columnconfigure(0,weight=1) self.rowconfigure(0,weight=1) self.last = None self.default_cursor = self['cursor'] self.configure(cursor="watch") def queue(self, q, w_artists, w_albums, w_titles, artists, albums, titles): # called with new queue state # delete old contents try: for i in self.canvas.find_all(): self.canvas.delete(i) except TclError: # if the call was queued but not received before the window was deleted # we might get an error from Tcl/Tk, which no longer knows the window, # here return w = tracklistHFont.measure("Artist") if w > w_artists: w_artists = w w = tracklistHFont.measure("Album") if w > w_albums: w_albums = w w = tracklistHFont.measure("Title") if w > w_titles: w_titles = w hheading = tracklistHFont.metrics("linespace") h = tracklistFont.metrics('linespace') x_artist = 8 x_album = x_artist + w_artists + 16 x_title = x_album + w_albums + 16 w = x_title + w_titles + 8 self.canvas['scrollregion'] = (0, 0, w, h * len(artists) + hheading) self.canvas.create_text(x_artist, 0, text="Artist", font=tracklistHFont, anchor='nw') self.canvas.create_text(x_album, 0, text="Album", font=tracklistHFont, anchor='nw') self.canvas.create_text(x_title, 0, text="Title", font=tracklistHFont, anchor='nw') y = hheading for n in range(0,len(artists)): artist = artists[n] album = albums[n] title = titles[n] if artist != "": self.canvas.create_text(x_artist, y, text=artist, font=tracklistFont, anchor='nw') if album != "": self.canvas.create_text(x_album, y, text=album, font=tracklistFont, anchor='nw') if title != "": self.canvas.create_text(x_title, y, text=title, font=tracklistFont, anchor='nw') y += h self.last = q self.configure(cursor=self.default_cursor) def poll(self, c): q = self.getqueue(c) if q != self.last: # we do the track name calculation in the background thread so that # the gui can still be responsive artists = [] albums = [] titles = [] w_artists = w_albums = w_titles = 16 for t in q: artist = part(c, t['track'], 'display', 'artist') album = part(c, t['track'], 'display', 'album') title = part(c, t['track'], 'display', 'title') w = tracklistFont.measure(artist) if w > w_artists: w_artists = w w = tracklistFont.measure(album) if w > w_albums: w_albums = w w = tracklistFont.measure(title) if w > w_titles: w_titles = w artists.append(artist) albums.append(album) titles.append(title) intercom.put(lambda: self.queue(q, w_artists, w_albums, w_titles, artists, albums, titles)) self.last = q class QueueWidget(TrackListWidget): def __init__(self, master=None, **kw): TrackListWidget.__init__(self, master, **kw) def getqueue(self, c): return c.queue() class RecentWidget(TrackListWidget): def __init__(self, master=None, **kw): TrackListWidget.__init__(self, master, **kw) def getqueue(self, c): l = c.recent() l.reverse() return l class MonitorStateThread: # thread to pick up current server state and publish it # # Creates a client and monitors it in a daemon thread for state changes. # Whenever one occurs, call w.poll(c) for every member w of widgets with # a client owned by the thread in which the call occurs. def __init__(self, widgets, masterclient=None): self.logclient = disorder.client() self.client = disorder.client() self.clientlock = threading.Lock() if not masterclient: masterclient = disorder.client() self.masterclient = masterclient self.widgets = widgets self.lock = threading.Lock() # the main thread self.thread = threading.Thread(target=self.run) self.thread.setDaemon(True) self.thread.start() # spare thread for processing additions self.adderq = Queue.Queue() self.adder = threading.Thread(target=self.runadder) self.adder.setDaemon(True) self.adder.start() def notify(self, line): self.lock.acquire() widgets = self.widgets self.lock.release() for w in widgets: self.clientlock.acquire() w.poll(self.client) self.clientlock.release() return 1 def add(self, w): self.lock.acquire() self.widgets.append(w) self.lock.release() self.adderq.put(lambda client: w.poll(client)) def remove(self, what): self.lock.acquire() self.widgets.remove(what) self.lock.release() def run(self): self.notify("") self.logclient.log(lambda client, line: self.notify(line)) def runadder(self): while True: item = self.adderq.get() self.clientlock.acquire() item(self.client) self.clientlock.release() ######################################################################## def usage(s): # display usage on S s.write( """Usage: tkdisorder [OPTIONS] Options: -h, --help Display this message -V, --version Display version number tkdisorder is copyright (c) 2004, 2005 Richard Kettlewell. """) ######################################################################## try: opts, rest = getopt.getopt(sys.argv[1:], "Vh", ["version", "help"]) except getopt.GetoptError, e: sys.stderr.write("ERROR: %s, try --help for help\n" % e.msg) sys.exit(1) for o, v in opts: if o in ('-V', '--version'): print "%s" % disorder.version sys.stdout.close() sys.exit(0) if o in ('h', '--help'): usage(sys.stdout) sys.stdout.close() sys.exit(0) client = disorder.client() # master thread's client root = Tk() root.title("DisOrder") tracklistFont = tkFont.Font(family='Helvetica', size=10) tracklistHFont = tracklistFont.copy() tracklistHFont.config(weight="bold") p = PlayingWidget(root) p.pack(fill=BOTH, expand=1) q = QueueWidget(root) q.pack(fill=BOTH, expand=1) intercom = Intercom(root) # only need a single intercom mst = MonitorStateThread([p, q], client) root.mainloop() # Local Variables: # py-indent-offset:2 # comment-column:40 # fill-column:79 # End: