chiark / gitweb /
Make all search results visible.
[disorder] / python / tkdisorder
CommitLineData
460b9539 1#! /usr/bin/env python
2#
3# Copyright (C) 2004, 2005 Richard Kettlewell
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful, but
11# WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18# USA
19#
20
ffaf09ca
RK
21# THIS PROGRAM IS NO LONGER MAINTAINED.
22#
23# It worked last time I tried running it, but all client maintenance
24# effort is now devoted to the web interface and the GTK+ client
25# (Disobedience).
26
460b9539 27"""Graphical user interface for DisOrder"""
28
29from Tkinter import *
30import tkFont
31import Queue
32import threading
33import disorder
34import time
35import string
36import re
37import getopt
38import sys
39
40########################################################################
41
42# Architecture:
43#
44# The main (initial) thread of the program runs all GUI code. The GUI is only
45# directly modified from inside this thread. We sometimes call this the
46# master thread.
47#
48# We have a background thread, MonitorStateThread, which waits for changes to
49# the server's state which we care about. Whenever such a change occurs it
50# notifies all the widgets which care about it (and possibly other widgets;
51# the current implementation is unsophisticated.)
52#
53# Widget poll() methods usually, but NOT ALWAYS, called in the
54# MonitorStateThread. Other widget methods are call in the master thread.
55#
56# We have a separate disorder.client for each thread rather than locking a
57# single client. MonitorStateThread also has a private disorder.client that
58# it uses to watch for server state changes.
59
60########################################################################
61
62class Intercom:
63 # communication queue into thread containing Tk event loop
64 #
65 # Sets up a callback on the event loop (in this thread) which periodically
66 # checks the queue for elements; if any are found they are executed.
67 def __init__(self, master):
68 self.q = Queue.Queue();
69 self.master = master
70 self.poll()
71
72 def poll(self):
73 try:
74 item = self.q.get_nowait()
75 item()
76 self.master.after_idle(self.poll)
77 except Queue.Empty:
78 self.master.after(100, self.poll)
79
80 def put(self, item):
81 self.q.put(item)
82
83########################################################################
84
85class ProgressBar(Canvas):
86 # progress bar widget
87 def __init__(self, master=None, **kw):
88 Canvas.__init__(self, master, highlightthickness=0, **kw)
89 self.outer = self.create_rectangle(0, 0, 0, 0,
90 outline="#000000",
91 width=1,
92 fill="#ffffff")
93 self.bar = self.create_rectangle(0, 0, 0, 0,
94 width=1,
95 fill="#ff0000",
96 outline='#ff0000')
97 self.current = None
98 self.total = None
99 self.bind("<Configure>", lambda e: self.redisplay())
100
101 def update(self, current, total):
102 self.current = current
103 if current > total:
104 current = total
105 elif current < 0:
106 current = 0
107 self.total = total
108 self.redisplay()
109
110 def clear(self):
111 self.current = None
112 self.total = None
113 self.redisplay()
114
115 def redisplay(self):
116 w, h = self.winfo_width(), self.winfo_height()
117 if w > 0 and h > 0:
118 self.coords(self.outer, 0, 0, w - 1, h - 1)
119 if self.total:
120 bw = int((w - 2) * self.current / self.total)
121 self.itemconfig(self.bar,
122 fill="#ff0000",
123 outline="#ff0000")
124 self.coords(self.bar, 1, 1, bw, h - 2)
125 else:
126 self.itemconfig(self.bar,
127 fill="#909090",
128 outline="#909090")
129 self.coords(self.bar, 1, 1, w - 2, h - 2)
130
131# look up a track's name part, using client c. Maintains a cache.
132part_cache = {}
133def part(c, track, context, part):
134 key = "%s-%s-%s" % (part, context, track)
135 now = time.time()
136 if not part_cache.has_key(key) or part_cache[key]['when'] < now - 3600:
137 part_cache[key] = {'when': now,
138 'what': c.part(track, context, part)}
139 return part_cache[key]['what']
140
141class PlayingWidget(Frame):
142 # widget that always displays information about what's
143 # playing
144 def __init__(self, master=None, **kw):
145 Frame.__init__(self, master, **kw)
146 # column 0 is descriptions, column 1 is the values
147 self.columnconfigure(0,weight=0)
148 self.columnconfigure(1,weight=1)
149 self.fields = {}
150 self.field(0, 0, "artist", "Artist")
151 self.field(1, 0, "album", "Album")
152 self.field(2, 0, "title", "Title")
153 # column 1 also has the progress bar in it
154 self.p = ProgressBar(self, height=20)
155 self.p.grid(row=3, column=1, sticky=E+W)
156 # column 2 has operation buttons
157 b = Button(self, text="Quit", command=self.quit)
158 b.grid(row=0, column=2, sticky=E+W)
159 b = Button(self, text="Scratch", command=self.scratch)
160 b.grid(row=1, column=2, sticky=E+W)
161 b = Button(self, text="Recent", command=self.recent)
162 b.grid(row=2, column=2, sticky=E+W)
163 self.length = 0
164 self.update_length()
165 self.last = None
166 self.recentw = None
167
168 def field(self, row, column, name, label):
169 # create a field
170 Label(self, text=label).grid(row=row, column=column, sticky=E)
171 self.fields[name] = Text(self, height=1, state=DISABLED)
172 self.fields[name].grid(row=row, column=column + 1, sticky=W+E);
173
174 def set(self, name, value):
175 # set a field's value
176 f = self.fields[name]
177 f.config(state=NORMAL)
178 f.delete(1.0, END)
179 f.insert(END, value)
180 f.config(state=DISABLED)
181
182 def playing(self, p):
183 # called with new what's-playing information
184 values = {}
185 if p:
186 for tpart in ['artist', 'album', 'title']:
187 values[tpart] = part(client, p['track'], 'display', tpart)
188 try:
189 self.length = client.length(p['track'])
190 except disorder.operationError:
191 self.length = 0
192 self.started = int(p['played'])
193 else:
194 self.length = 0
195 for k in self.fields.keys():
196 if k in values:
197 self.set(k, values[k])
198 else:
199 self.set(k, "")
200 self.length_bar()
201
202 def length_bar(self):
203 if self.length and self.length > 0:
204 self.p.update(time.time() - self.started, self.length)
205 else:
206 self.p.clear()
207
208 def update_length(self):
209 self.length_bar()
210 self.after(1000, self.update_length)
211
212 def poll(self, c):
213 p = c.playing()
214 if p != self.last:
215 intercom.put(lambda: self.playing(p))
216 self.last = p
217
218 def quit(self):
219 sys.exit(0)
220
221 def scratch(self):
222 client.scratch()
223
224 def recent_close(self):
225 self.recentw.destroy()
226 self.recentw = None
227
228 def recent(self):
229 if self.recentw:
230 self.recentw.deiconify()
231 self.recentw.lift()
232 else:
233 w = 80*tracklistFont.measure('A')
234 h = 40*tracklistFont.metrics("linespace")
235 self.recentw = Toplevel()
236 self.recentw.protocol("WM_DELETE_WINDOW", self.recent_close)
237 self.recentw.title("Recently Played")
238 # XXX for some reason Toplevel(width=w,height=h) doesn't seem to work
239 self.recentw.geometry("%dx%d" % (w,h))
240 w = RecentWidget(self.recentw)
241 w.pack(fill=BOTH, expand=1)
242 mst.add(w);
243
244class TrackListWidget(Frame):
245 def __init__(self, master=None, **kw):
246 Frame.__init__(self, master, **kw)
247 self.yscrollbar = Scrollbar(self)
248 self.xscrollbar = Scrollbar(self, orient=HORIZONTAL)
249 self.canvas = Canvas(self,
250 xscrollcommand=self.xscrollbar.set,
251 yscrollcommand=self.yscrollbar.set)
252 self.xscrollbar.config(command=self.canvas.xview)
253 self.yscrollbar.config(command=self.canvas.yview)
254 self.canvas.grid(row=0, column=0, sticky=N+S+E+W)
255 self.yscrollbar.grid(row=0, column=1, sticky=N+S)
256 self.xscrollbar.grid(row=1, column=0, sticky=E+W)
257 self.columnconfigure(0,weight=1)
258 self.rowconfigure(0,weight=1)
259 self.last = None
260 self.default_cursor = self['cursor']
261 self.configure(cursor="watch")
262
263 def queue(self, q, w_artists, w_albums, w_titles, artists, albums, titles):
264 # called with new queue state
265 # delete old contents
266 try:
267 for i in self.canvas.find_all():
268 self.canvas.delete(i)
269 except TclError:
270 # if the call was queued but not received before the window was deleted
271 # we might get an error from Tcl/Tk, which no longer knows the window,
272 # here
273 return
274 w = tracklistHFont.measure("Artist")
275 if w > w_artists:
276 w_artists = w
277 w = tracklistHFont.measure("Album")
278 if w > w_albums:
279 w_albums = w
280 w = tracklistHFont.measure("Title")
281 if w > w_titles:
282 w_titles = w
283 hheading = tracklistHFont.metrics("linespace")
284 h = tracklistFont.metrics('linespace')
285 x_artist = 8
286 x_album = x_artist + w_artists + 16
287 x_title = x_album + w_albums + 16
288 w = x_title + w_titles + 8
289 self.canvas['scrollregion'] = (0, 0, w, h * len(artists) + hheading)
290 self.canvas.create_text(x_artist, 0, text="Artist",
291 font=tracklistHFont,
292 anchor='nw')
293 self.canvas.create_text(x_album, 0, text="Album",
294 font=tracklistHFont,
295 anchor='nw')
296 self.canvas.create_text(x_title, 0, text="Title",
297 font=tracklistHFont,
298 anchor='nw')
299 y = hheading
300 for n in range(0,len(artists)):
301 artist = artists[n]
302 album = albums[n]
303 title = titles[n]
304 if artist != "":
305 self.canvas.create_text(x_artist, y, text=artist,
306 font=tracklistFont,
307 anchor='nw')
308 if album != "":
309 self.canvas.create_text(x_album, y, text=album,
310 font=tracklistFont,
311 anchor='nw')
312 if title != "":
313 self.canvas.create_text(x_title, y, text=title,
314 font=tracklistFont,
315 anchor='nw')
316 y += h
317 self.last = q
318 self.configure(cursor=self.default_cursor)
319
320 def poll(self, c):
321 q = self.getqueue(c)
322 if q != self.last:
323 # we do the track name calculation in the background thread so that
324 # the gui can still be responsive
325 artists = []
326 albums = []
327 titles = []
328 w_artists = w_albums = w_titles = 16
329 for t in q:
330 artist = part(c, t['track'], 'display', 'artist')
331 album = part(c, t['track'], 'display', 'album')
332 title = part(c, t['track'], 'display', 'title')
333 w = tracklistFont.measure(artist)
334 if w > w_artists:
335 w_artists = w
336 w = tracklistFont.measure(album)
337 if w > w_albums:
338 w_albums = w
339 w = tracklistFont.measure(title)
340 if w > w_titles:
341 w_titles = w
342 artists.append(artist)
343 albums.append(album)
344 titles.append(title)
345 intercom.put(lambda: self.queue(q, w_artists, w_albums, w_titles,
346 artists, albums, titles))
347 self.last = q
348
349class QueueWidget(TrackListWidget):
350 def __init__(self, master=None, **kw):
351 TrackListWidget.__init__(self, master, **kw)
352
353 def getqueue(self, c):
354 return c.queue()
355
356class RecentWidget(TrackListWidget):
357 def __init__(self, master=None, **kw):
358 TrackListWidget.__init__(self, master, **kw)
359
360 def getqueue(self, c):
361 l = c.recent()
362 l.reverse()
363 return l
364
365class MonitorStateThread:
366 # thread to pick up current server state and publish it
367 #
368 # Creates a client and monitors it in a daemon thread for state changes.
369 # Whenever one occurs, call w.poll(c) for every member w of widgets with
370 # a client owned by the thread in which the call occurs.
371 def __init__(self, widgets, masterclient=None):
372 self.logclient = disorder.client()
373 self.client = disorder.client()
374 self.clientlock = threading.Lock()
375 if not masterclient:
376 masterclient = disorder.client()
377 self.masterclient = masterclient
378 self.widgets = widgets
379 self.lock = threading.Lock()
380 # the main thread
381 self.thread = threading.Thread(target=self.run)
382 self.thread.setDaemon(True)
383 self.thread.start()
384 # spare thread for processing additions
385 self.adderq = Queue.Queue()
386 self.adder = threading.Thread(target=self.runadder)
387 self.adder.setDaemon(True)
388 self.adder.start()
389
390 def notify(self, line):
391 self.lock.acquire()
392 widgets = self.widgets
393 self.lock.release()
394 for w in widgets:
395 self.clientlock.acquire()
396 w.poll(self.client)
397 self.clientlock.release()
398 return 1
399
400 def add(self, w):
401 self.lock.acquire()
402 self.widgets.append(w)
403 self.lock.release()
404 self.adderq.put(lambda client: w.poll(client))
405
406 def remove(self, what):
407 self.lock.acquire()
408 self.widgets.remove(what)
409 self.lock.release()
410
411 def run(self):
412 self.notify("")
413 self.logclient.log(lambda client, line: self.notify(line))
414
415 def runadder(self):
416 while True:
417 item = self.adderq.get()
418 self.clientlock.acquire()
419 item(self.client)
420 self.clientlock.release()
421
422########################################################################
423
424def usage(s):
425 # display usage on S
426 s.write(
427 """Usage:
428
429 tkdisorder [OPTIONS]
430
431Options:
432
433 -h, --help Display this message
434 -V, --version Display version number
435
436tkdisorder is copyright (c) 2004, 2005 Richard Kettlewell.
437""")
438
439########################################################################
440
441try:
442 opts, rest = getopt.getopt(sys.argv[1:], "Vh", ["version", "help"])
443except getopt.GetoptError, e:
444 sys.stderr.write("ERROR: %s, try --help for help\n" % e.msg)
445 sys.exit(1)
446for o, v in opts:
447 if o in ('-V', '--version'):
448 print "%s" % disorder.version
449 sys.stdout.close()
450 sys.exit(0)
451 if o in ('h', '--help'):
452 usage(sys.stdout)
453 sys.stdout.close()
454 sys.exit(0)
455
456client = disorder.client() # master thread's client
457
458root = Tk()
459root.title("DisOrder")
460
461tracklistFont = tkFont.Font(family='Helvetica', size=10)
462tracklistHFont = tracklistFont.copy()
463tracklistHFont.config(weight="bold")
464
465p = PlayingWidget(root)
466p.pack(fill=BOTH, expand=1)
467
468q = QueueWidget(root)
469q.pack(fill=BOTH, expand=1)
470
471intercom = Intercom(root) # only need a single intercom
472mst = MonitorStateThread([p, q], client)
473
474root.mainloop()
475
476# Local Variables:
477# py-indent-offset:2
478# comment-column:40
479# fill-column:79
480# End: