3 # Copyright (C) 2004, 2005 Richard Kettlewell
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.
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.
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
21 # THIS PROGRAM IS NO LONGER MAINTAINED.
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
27 """Graphical user interface for DisOrder"""
40 ########################################################################
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
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.)
53 # Widget poll() methods usually, but NOT ALWAYS, called in the
54 # MonitorStateThread. Other widget methods are call in the master thread.
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.
60 ########################################################################
63 # communication queue into thread containing Tk event loop
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();
74 item
= self
.q
.get_nowait()
76 self
.master
.after_idle(self
.poll
)
78 self
.master
.after(100, self
.poll
)
83 ########################################################################
85 class ProgressBar(Canvas
):
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,
93 self
.bar
= self
.create_rectangle(0, 0, 0, 0,
99 self
.bind("<Configure>", lambda e
: self
.redisplay())
101 def update(self
, current
, total
):
102 self
.current
= current
116 w
, h
= self
.winfo_width(), self
.winfo_height()
118 self
.coords(self
.outer
, 0, 0, w
- 1, h
- 1)
120 bw
= int((w
- 2) * self
.current
/ self
.total
)
121 self
.itemconfig(self
.bar
,
124 self
.coords(self
.bar
, 1, 1, bw
, h
- 2)
126 self
.itemconfig(self
.bar
,
129 self
.coords(self
.bar
, 1, 1, w
- 2, h
- 2)
131 # look up a track's name part, using client c. Maintains a cache.
133 def part(c
, track
, context
, part
):
134 key
= "%s-%s-%s" %
(part
, context
, track
)
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']
141 class PlayingWidget(Frame
):
142 # widget that always displays information about what's
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)
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
)
168 def field(self
, row
, column
, name
, label
):
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
);
174 def set(self
, name
, value
):
175 # set a field's value
176 f
= self
.fields
[name
]
177 f
.config(state
=NORMAL
)
180 f
.config(state
=DISABLED
)
182 def playing(self
, p
):
183 # called with new what's-playing information
186 for tpart
in ['artist', 'album', 'title']:
187 values
[tpart
] = part(client
, p
['track'], 'display', tpart
)
189 self
.length
= client
.length(p
['track'])
190 except disorder
.operationError
:
192 self
.started
= int(p
['played'])
195 for k
in self
.fields
.keys():
197 self
.set(k
, values
[k
])
202 def length_bar(self
):
203 if self
.length
and self
.length
> 0:
204 self
.p
.update(time
.time() - self
.started
, self
.length
)
208 def update_length(self
):
210 self
.after(1000, self
.update_length
)
215 intercom
.put(lambda: self
.playing(p
))
224 def recent_close(self
):
225 self
.recentw
.destroy()
230 self
.recentw
.deiconify()
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)
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)
260 self
.default_cursor
= self
['cursor']
261 self
.configure(cursor
="watch")
263 def queue(self
, q
, w_artists
, w_albums
, w_titles
, artists
, albums
, titles
):
264 # called with new queue state
265 # delete old contents
267 for i
in self
.canvas
.find_all():
268 self
.canvas
.delete(i
)
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,
274 w
= tracklistHFont
.measure("Artist")
277 w
= tracklistHFont
.measure("Album")
280 w
= tracklistHFont
.measure("Title")
283 hheading
= tracklistHFont
.metrics("linespace")
284 h
= tracklistFont
.metrics('linespace')
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",
293 self
.canvas
.create_text(x_album
, 0, text
="Album",
296 self
.canvas
.create_text(x_title
, 0, text
="Title",
300 for n
in range(0,len(artists
)):
305 self
.canvas
.create_text(x_artist
, y
, text
=artist
,
309 self
.canvas
.create_text(x_album
, y
, text
=album
,
313 self
.canvas
.create_text(x_title
, y
, text
=title
,
318 self
.configure(cursor
=self
.default_cursor
)
323 # we do the track name calculation in the background thread so that
324 # the gui can still be responsive
328 w_artists
= w_albums
= w_titles
= 16
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
)
336 w
= tracklistFont
.measure(album
)
339 w
= tracklistFont
.measure(title
)
342 artists
.append(artist
)
345 intercom
.put(lambda: self
.queue(q
, w_artists
, w_albums
, w_titles
,
346 artists
, albums
, titles
))
349 class QueueWidget(TrackListWidget
):
350 def __init__(self
, master
=None, **kw
):
351 TrackListWidget
.__init__(self
, master
, **kw
)
353 def getqueue(self
, c
):
356 class RecentWidget(TrackListWidget
):
357 def __init__(self
, master
=None, **kw
):
358 TrackListWidget
.__init__(self
, master
, **kw
)
360 def getqueue(self
, c
):
365 class MonitorStateThread
:
366 # thread to pick up current server state and publish it
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()
376 masterclient
= disorder
.client()
377 self
.masterclient
= masterclient
378 self
.widgets
= widgets
379 self
.lock
= threading
.Lock()
381 self
.thread
= threading
.Thread(target
=self
.run
)
382 self
.thread
.setDaemon(True)
384 # spare thread for processing additions
385 self
.adderq
= Queue
.Queue()
386 self
.adder
= threading
.Thread(target
=self
.runadder
)
387 self
.adder
.setDaemon(True)
390 def notify(self
, line
):
392 widgets
= self
.widgets
395 self
.clientlock
.acquire()
397 self
.clientlock
.release()
402 self
.widgets
.append(w
)
404 self
.adderq
.put(lambda client
: w
.poll(client
))
406 def remove(self
, what
):
408 self
.widgets
.remove(what
)
413 self
.logclient
.log(lambda client
, line
: self
.notify(line
))
417 item
= self
.adderq
.get()
418 self
.clientlock
.acquire()
420 self
.clientlock
.release()
422 ########################################################################
433 -h, --help Display this message
434 -V, --version Display version number
436 tkdisorder is copyright (c) 2004, 2005 Richard Kettlewell.
439 ########################################################################
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
)
447 if o
in ('-V', '--version'):
448 print "%s" % disorder
.version
451 if o
in ('h', '--help'):
456 client
= disorder
.client() # master thread's client
459 root
.title("DisOrder")
461 tracklistFont
= tkFont
.Font(family
='Helvetica', size
=10)
462 tracklistHFont
= tracklistFont
.copy()
463 tracklistHFont
.config(weight
="bold")
465 p
= PlayingWidget(root
)
466 p
.pack(fill
=BOTH
, expand
=1)
468 q
= QueueWidget(root
)
469 q
.pack(fill
=BOTH
, expand
=1)
471 intercom
= Intercom(root
) # only need a single intercom
472 mst
= MonitorStateThread([p
, q
], client
)