Commit | Line | Data |
---|---|---|
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 | ||
29 | from Tkinter import * | |
30 | import tkFont | |
31 | import Queue | |
32 | import threading | |
33 | import disorder | |
34 | import time | |
35 | import string | |
36 | import re | |
37 | import getopt | |
38 | import 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 | ||
62 | class 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 | ||
85 | class 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. | |
132 | part_cache = {} | |
133 | def 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 | ||
141 | class 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 | ||
244 | class 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 | ||
349 | class 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 | ||
356 | class 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 | ||
365 | class 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 | ||
424 | def usage(s): | |
425 | # display usage on S | |
426 | s.write( | |
427 | """Usage: | |
428 | ||
429 | tkdisorder [OPTIONS] | |
430 | ||
431 | Options: | |
432 | ||
433 | -h, --help Display this message | |
434 | -V, --version Display version number | |
435 | ||
436 | tkdisorder is copyright (c) 2004, 2005 Richard Kettlewell. | |
437 | """) | |
438 | ||
439 | ######################################################################## | |
440 | ||
441 | try: | |
442 | opts, rest = getopt.getopt(sys.argv[1:], "Vh", ["version", "help"]) | |
443 | except getopt.GetoptError, e: | |
444 | sys.stderr.write("ERROR: %s, try --help for help\n" % e.msg) | |
445 | sys.exit(1) | |
446 | for 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 | ||
456 | client = disorder.client() # master thread's client | |
457 | ||
458 | root = Tk() | |
459 | root.title("DisOrder") | |
460 | ||
461 | tracklistFont = tkFont.Font(family='Helvetica', size=10) | |
462 | tracklistHFont = tracklistFont.copy() | |
463 | tracklistHFont.config(weight="bold") | |
464 | ||
465 | p = PlayingWidget(root) | |
466 | p.pack(fill=BOTH, expand=1) | |
467 | ||
468 | q = QueueWidget(root) | |
469 | q.pack(fill=BOTH, expand=1) | |
470 | ||
471 | intercom = Intercom(root) # only need a single intercom | |
472 | mst = MonitorStateThread([p, q], client) | |
473 | ||
474 | root.mainloop() | |
475 | ||
476 | # Local Variables: | |
477 | # py-indent-offset:2 | |
478 | # comment-column:40 | |
479 | # fill-column:79 | |
480 | # End: |