#! /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 3 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, see .
#
# 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: