Theme changes, fix for unicode conversion errors, misc
[rox-musicbox.git] / musicbox.py
bloba18ed10fb1966d5cf35b2448a0e5bc779b1a69c1
1 """
2 musicbox.py
4 Copyright 2004 Kenneth Hayber <ken@hayber.us>
5 All rights reserved.
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License.
11 This program is distributed in the hope that it will be useful
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
21 from __future__ import generators
23 import os, sys, re, string, threading, pango, gtk, gobject
24 from threading import *
26 import rox
27 from rox import Menu, app_options, loading, saving, InfoWin, OptionsBox, filer
28 from rox.options import Option
31 try:
32 import player, playlist, playlistui, xsoap, plugins
33 except:
34 rox.report_exception()
37 #Who am I and how did I get here?
38 APP_NAME = "MusicBox"
39 APP_DIR = rox.app_dir
40 APP_DOMAIN = 'hayber.us'
42 ALBUM_COVER_SIZE = 90
44 #Toolbar button indexes
45 BTN_CLOSE = 0
46 BTN_PREV = 1
47 BTN_PLAY = 2
48 BTN_STOP = 3
49 BTN_NEXT = 4
50 BTN_REPEAT = 5
51 BTN_SHUFFLE = 6
52 BTN_PLAYLIST = 7
53 BTN_OPTIONS = 8
56 #Bitmaps (potentially) used in this application
57 factory = gtk.IconFactory()
58 for name in [
59 # uncomment these two lines to use the icons in 'images' directory instead of GTK theme
60 # 'gtk-media-stop', 'gtk-media-pause', 'gtk-media-play', 'gtk-media-record',
61 # 'gtk-media-next', 'gtk-media-previous', 'gtk-media-forward', 'gtk-media-rewind',
62 # 'media-eject',
63 'media-repeat', 'media-shuffle',
64 # 'media_track', 'stock_playlist',
65 # 'stock_volume-max', 'stock_volume-med', 'stock_volume-min',
66 # 'stock_volume-mute', 'stock_volume-0'
68 path = os.path.join(rox.app_dir, "images", name + ".svg")
69 pixbuf = gtk.gdk.pixbuf_new_from_file(path)
70 if not pixbuf:
71 print >>sys.stderr, "Can't load stock icon '%s'" % name
72 else:
73 gtk.stock_add([(name, name, 0, 0, "")])
74 factory.add(name, gtk.IconSet(pixbuf = pixbuf))
75 factory.add_default()
78 #Options.xml processing
79 from rox import choices
80 choices.migrate(APP_NAME, APP_DOMAIN)
81 rox.setup_app_options(APP_NAME, site=APP_DOMAIN)
82 Menu.set_save_name(APP_NAME, site=APP_DOMAIN)
84 #assume that everyone puts their music in ~/Music
85 LIBRARY = Option('library', os.path.expanduser('~')+'/Music')
87 #how to parse each library leaf to get artist, album, title...
88 LIBRARY_RE = Option('library_re', '^.*/(?P<artist>.*)/(?P<album>.*)/(?P<title>.*)')
90 #the native driver type you want to use ('ao', 'alsa', 'oss', 'linux')
91 DRIVER_TYPE = Option('driver_type', 'alsa')
92 #the ao driver type you want to use ('esd', 'oss', 'alsa', 'alsa09')
93 DRIVER_ID = Option('driver_id', 'alsa')
94 SOUND_DEVICE = Option('sound_device', 'default') # '/dev/mixer' or 'default'
96 MIXER_DEVICE = Option('mixer_device', 'default') # '/dev/mixer' or 'default'
97 MIXER_CHANNEL = Option('mixer_channel', 'PCM')
99 SHUFFLE = Option('shuffle', 0)
100 REPEAT = Option('repeat', 0)
102 #Don't replay any of the last n songs in shuffle mode
103 SHUFFLE_CACHE_SIZE = Option('shuffle_cache', 10)
105 #Buffer size used by audio device read/write
106 AUDIO_BUFFER_SIZE = Option('audio_buffer', 4096)
108 #Eye candy
109 SONG_FONT = Option('song_font', None)
110 BASE_FONT = Option('base_font', None)
111 BG_COLOR = Option('bg_color', '#A6A699')
112 FG_COLOR = Option('fg_color', '#000000')
114 #Show/Hide settings
115 SH_TOOLBAR = Option('toolbar', True)
116 SH_VOLUME = Option('volume', True)
117 SH_SEEKBAR = Option('seekbar', True)
119 #Other GUI details
120 WORDWRAP = Option('word_wrap', False)
121 MINITOOLS = Option('mini_toolbar', False)
122 TIMEDISPLAY = Option('time_display', False)
123 ALBUM_ART = Option('album_art', True)
125 tooltips = gtk.Tooltips()
128 def build_tool_options(box, node, label, option):
129 """Custom Option widget to allow show/hide each toolbar button"""
131 toolbar = gtk.Toolbar()
132 toolbar.set_style(gtk.TOOLBAR_ICONS)
134 done = False
135 def item_changed(thing):
136 if done: #supress widget updates until later
137 box.check_widget(option)
139 type1 = gtk.TOOLBAR_CHILD_TOGGLEBUTTON
140 buttons = [
141 #(type, text, icon, callback)
142 (type1, _("Close"), gtk.STOCK_CLOSE, item_changed),
143 (type1, _("Prev"), gtk.STOCK_MEDIA_PREVIOUS, item_changed),
144 (type1, _("Play"), gtk.STOCK_MEDIA_PLAY, item_changed),
145 (type1, _("Stop"), gtk.STOCK_MEDIA_STOP, item_changed),
146 (type1, _("Next"), gtk.STOCK_MEDIA_NEXT, item_changed),
147 (type1, _("Repeat"), 'media-repeat', item_changed),
148 (type1, _("Shuffle"), 'media-shuffle', item_changed),
149 (type1, _("Playlist"), gtk.STOCK_INDEX, item_changed),
150 (type1, _("Options"), gtk.STOCK_PREFERENCES, item_changed),
153 controls = []
154 for (type, text, icon, callback) in buttons:
155 image = gtk.image_new_from_stock(icon, gtk.ICON_SIZE_LARGE_TOOLBAR)
156 controls.append(toolbar.insert_element(type, None, text, text,
157 None, image, callback, None, -1))
159 #build a bitmask of which buttons are active
160 def get_values():
161 value = 0
162 for i in range(len(controls)):
163 if controls[i].get_active():
164 value |= (1 << i)
165 return value
166 def set_values(): pass
167 box.handlers[option] = (get_values, set_values)
169 #initialize the button states
170 for i in range(len(controls)):
171 if option.int_value & (1 << i):
172 controls[i].set_active(True)
173 done = True #OK, updates activated.
175 box.may_add_tip(toolbar, node)
176 return [toolbar]
178 OptionsBox.widget_registry['tool_options'] = build_tool_options
179 TOOLOPTIONS = Option('toolbar_disable', -1)
181 rox.app_options.notify()
184 class MusicBox(rox.Window, loading.XDSLoader):
185 """A Music Player for mp3 and ogg - main class"""
186 time_string = ''
187 _shuffle = False
188 _repeat = False
191 def __init__(self):
192 """Constructor for MusicBox"""
193 rox.Window.__init__(self)
194 loading.XDSLoader.__init__(self, plugins.TYPE_LIST)
196 # Main window settings
197 self.set_title(APP_NAME)
198 self.set_role("MainWindow")
200 # Notifications
201 rox.app_options.add_notify(self.get_options)
202 self.connect('delete_event', self.delete_event)
203 self.connect('window-state-event', self.window_state_event)
204 self.connect('drag-motion', self.xds_drag_motion)
206 # Set some defaults
207 self.replace_library = False
208 self.library = LIBRARY.value.split(':')
209 self.playlist = None
210 self.playlistUI = None
211 self.current_song = None
213 self.shuffle = bool(SHUFFLE.int_value)
214 self.repeat = bool(REPEAT.int_value)
216 # Build and Init everything
217 #GTK2.4 self.uimanager = gtk.UIManager()
218 #GTK2.4 self.uimanager.insert_action_group(self.build_actions(), 0)
219 #GTK2.4 self.uimanager.add_ui_from_file('ui.xml')
220 #GTK2.4 self.menu = self.uimanager.get_widget('/ui/popup')
221 #GTK2.4 self.toolbar = self.uimanager.get_widget('/ui/toolbar')
222 #GTK2.4 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
223 #GTK2.4 self.connect('button-press-event', self.button_press)
224 #GTK2.4 self.connect('popup-menu', self.menukey_press)
226 self.build_menu()
227 self.build_toolbar()
228 self.build_labels()
229 self.set_line_wrap()
230 self.build_misc()
231 self.set_fonts()
232 self.set_colors()
234 # Pack and show widgets
235 self.vbox = gtk.VBox()
236 self.hbox = gtk.HBox()
237 self.add(self.vbox)
238 self.vbox.add(self.hbox)
240 self.hbox.pack_start(self.display, True, True, 0)
241 self.hbox.pack_end(self.volume_control, False, True, 0)
242 self.vbox.pack_end(self.toolbar, False, True, 0)
243 self.vbox.pack_end(self.seek_bar_control, False, True, 0)
245 self.vbox.show_all()
246 self.show_hide_controls()
247 self.show_hide_buttons()
249 if not ALBUM_ART.int_value:
250 self.album_img.hide()
251 else:
252 self.display.set_size_request(ALBUM_COVER_SIZE, ALBUM_COVER_SIZE)
254 self.show()
256 #Start xmlrpc server
257 self.server()
259 self.playlist = playlist.Playlist(SHUFFLE_CACHE_SIZE.int_value, LIBRARY_RE.value)
260 self.player = player.Player(DRIVER_TYPE.value, DRIVER_ID.value, AUDIO_BUFFER_SIZE.int_value, SOUND_DEVICE.value)
261 self.foo = Thread(name='player', target=self.player.run)
262 self.foo.setDaemon(True)
263 self.foo.start()
264 self.volume.set_value(self.player.get_volume(MIXER_DEVICE.value, MIXER_CHANNEL.value))
266 if len(sys.argv) > 1:
267 self.load_args(sys.argv[1:], True)
268 else:
269 self.load_args([], False)
271 gobject.timeout_add(500, self.display_update)
273 #GTK2.4 def build_actions(self):
274 #GTK2.4 actions = gtk.ActionGroup('main')
275 #GTK2.4 actions.add_action(gtk.Action('quit', _("Quit"), _("Quit the application"), gtk.STOCK_QUIT))
276 #GTK2.4 actions.add_action(gtk.Action('close', _("Close"), _("Close this window"), gtk.STOCK_CLOSE))
277 #GTK2.4
278 #GTK2.4 actions.add_action(gtk.Action('options', _("Options"), _("Edit Options"), gtk.STOCK_PREFERENCES))
279 #GTK2.4 actions.add_action(gtk.Action('info', _("Info"), _("Show program info"), gtk.STOCK_DIALOG_INFO))
280 #GTK2.4
281 #GTK2.4 actions.add_action(gtk.Action('play', _("Play"), _("Play"), 'media-play'))
282 #GTK2.4 actions.add_action(gtk.Action('stop', _("Stop"), _("Stop"), 'media-stop'))
283 #GTK2.4 actions.add_action(gtk.Action('prev', _("Prev"), _("Prev"), 'media-prev'))
284 #GTK2.4 actions.add_action(gtk.Action('next', _("Next"), _("Next"), 'media-next'))
285 #GTK2.4
286 #GTK2.4 actions.add_action(gtk.Action('playlist', _("Playlist"), _("Show Playlist"), gtk.STOCK_INDEX))
287 #GTK2.4 actions.add_action(gtk.Action('open', _("Open"), _("Open Location"), gtk.STOCK_GO_UP))
288 #GTK2.4 actions.add_action(gtk.Action('save', _("Save"), _("Save Playlist"), gtk.STOCK_SAVE))
289 #GTK2.4
290 #GTK2.4 actions.add_action(gtk.Action('shuffle', _("Shuffle"), _("Shuffle"), 'media-shuffle'))
291 #GTK2.4 actions.add_action(gtk.Action('repeat', _("Repeat"), _("Repeat"), 'media-repeat'))
292 #GTK2.4
293 #GTK2.4 actions.add_action(gtk.Action('none', None, None, 0))
294 #GTK2.4 return actions
297 def build_menu(self):
298 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
299 self.connect('button-press-event', self.button_press)
300 self.connect('popup-menu', self.menukey_press)
301 self.menu = Menu.Menu('main', [
302 Menu.Action(_("Play")+'\/'+_("Pause"), 'play_pause', '', gtk.STOCK_MEDIA_PLAY),
303 Menu.Action(_("Stop"), 'stop', '', gtk.STOCK_MEDIA_STOP),
304 Menu.Separator(),
306 Menu.Action(_("Back"), 'prev', '', gtk.STOCK_MEDIA_PREVIOUS),
307 Menu.Action(_("Next"), 'next', '', gtk.STOCK_MEDIA_NEXT),
308 Menu.Separator(),
310 Menu.SubMenu(_('Playlist'), [
311 Menu.Action(_("Show"), 'show_playlist', '', gtk.STOCK_INDEX),
312 Menu.Action(_("Open location"), 'show_dir', '', gtk.STOCK_GO_UP),
313 Menu.Action(_("Save"), 'save', '', gtk.STOCK_SAVE),
314 Menu.ToggleItem(_("Shuffle"), 'shuffle'),
315 Menu.ToggleItem(_("Repeat"), 'repeat'),
318 Menu.Separator(),
319 Menu.Action(_("Options"), 'show_options', '', gtk.STOCK_PREFERENCES),
320 Menu.Action(_('Info'), 'get_info', '', gtk.STOCK_DIALOG_INFO),
321 Menu.Separator(),
323 Menu.Action(_("Quit"), 'close', '', gtk.STOCK_CLOSE),
325 self.menu.attach(self,self)
328 def build_toolbar(self):
329 self.toolbar = gtk.Toolbar()
330 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
331 if bool(MINITOOLS.int_value):
332 self.toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR)
333 else:
334 self.toolbar.set_icon_size(gtk.ICON_SIZE_LARGE_TOOLBAR)
336 type1 = gtk.TOOLBAR_CHILD_BUTTON
337 type2 = gtk.TOOLBAR_CHILD_TOGGLEBUTTON
339 items = [
340 #(type, text, icon, callback)
341 (type1, _("Close"), gtk.STOCK_CLOSE, self.close),
342 (type1, _("Prev"), gtk.STOCK_MEDIA_PREVIOUS, self.prev),
343 (type1, _("Play"), gtk.STOCK_MEDIA_PLAY, self.play_pause),
344 (type1, _("Stop"), gtk.STOCK_MEDIA_STOP, self.stop),
345 (type1, _("Next"), gtk.STOCK_MEDIA_NEXT, self.next),
346 (type2, _("Repeat"), 'media-repeat', lambda b: self.set_repeat(b.get_active())),
347 (type2, _("Shuffle"), 'media-shuffle', lambda b: self.set_shuffle(b.get_active())),
348 (type1, _("Playlist"), gtk.STOCK_INDEX, self.show_playlist),
349 (type1, _("Options"), gtk.STOCK_PREFERENCES, self.show_options),
352 buttons = []
353 for (type, text, icon, callback) in items:
354 image = gtk.image_new_from_stock(icon, self.toolbar.get_icon_size())
355 buttons.append(self.toolbar.insert_element(type, None, text, text,
356 None, image, callback, None, -1))
357 if text == _("Play"): #this one changes later, so we have to save it
358 self.image_play = image
360 self.buttons = buttons
362 buttons[BTN_REPEAT].set_active(self.repeat)
363 buttons[BTN_SHUFFLE].set_active(self.shuffle)
364 buttons[BTN_PLAYLIST].set_sensitive(False)
367 def build_labels(self):
368 self.display = gtk.Layout()
369 self.display_size = (0, 0)
370 self.display.connect('size-allocate', self.resize)
372 self.album_img = gtk.Image()
373 self.album_img.set_alignment(0.0, 0.0)
375 hbox = gtk.HBox(False, 5)
376 vbox = gtk.VBox()
377 hbox.pack_start(self.album_img, False, False, 0)
378 hbox.pack_end(vbox, True, True, 0)
379 self.display.put(hbox, 0, 0)
380 self.display_box = vbox
382 self.display_song = gtk.Label()
383 self.display_song.set_alignment(0.0, 0.0)
385 self.display_album = gtk.Label()
386 self.display_album.set_alignment(0.0, 0.0)
388 self.display_artist = gtk.Label()
389 self.display_artist.set_alignment(0.0, 0.0)
391 self.display_status = gtk.Label()
392 self.display_status.set_alignment(0.0, 0.0)
394 vbox.pack_start(self.display_song, False, True, 0)
395 vbox.pack_start(self.display_album, False, True, 0)
396 vbox.pack_start(self.display_artist, False, True, 0)
397 vbox.pack_start(self.display_status, False, True, 0)
400 def set_line_wrap(self):
401 self.display_song.set_line_wrap(bool(WORDWRAP.int_value))
402 self.display_album.set_line_wrap(bool(WORDWRAP.int_value))
403 self.display_artist.set_line_wrap(bool(WORDWRAP.int_value))
404 self.display_status.set_line_wrap(bool(WORDWRAP.int_value))
407 def build_misc(self):
408 self.volume = gtk.Adjustment(50.0, 0.0, 100.0, 1.0, 10.0, 0.0)
409 self.volume.connect('value_changed', self.adjust_volume)
410 self.volume_control = gtk.VScale(self.volume)
411 self.volume_control.set_draw_value(False)
412 self.volume_control.set_inverted(True)
413 self.volume_control.set_size_request(-1, 90)
415 self.seek_bar = gtk.Adjustment(0.0, 0.0, 1.0, 0.01, 0.1, 0.0)
416 self.seek_id = self.seek_bar.connect('value_changed', self.adjust_seek_bar)
417 self.seek_bar_control = gtk.HScale(self.seek_bar)
418 self.seek_bar_control.set_update_policy(gtk.UPDATE_DELAYED)
419 self.seek_bar_control.set_draw_value(False)
420 self.seek_bar_control.set_size_request(100, -1)
423 def set_fonts(self):
424 song_font = pango.FontDescription(SONG_FONT.value)
425 base_font = pango.FontDescription(BASE_FONT.value)
426 self.display_song.modify_font(song_font)
427 self.display_album.modify_font(base_font)
428 self.display_artist.modify_font(base_font)
429 self.display_status.modify_font(base_font)
432 def set_colors(self):
433 fg_color = gtk.gdk.color_parse(FG_COLOR.value)
434 bg_color = gtk.gdk.color_parse(BG_COLOR.value)
435 self.display.modify_bg(gtk.STATE_NORMAL, bg_color)
436 self.display_song.modify_fg(gtk.STATE_NORMAL, fg_color)
437 self.display_album.modify_fg(gtk.STATE_NORMAL, fg_color)
438 self.display_artist.modify_fg(gtk.STATE_NORMAL, fg_color)
439 self.display_status.modify_fg(gtk.STATE_NORMAL, fg_color)
442 def show_hide_controls(self):
443 for (option, control) in [
444 (SH_VOLUME, self.volume_control), (SH_TOOLBAR, self.toolbar),
445 (SH_SEEKBAR, self.seek_bar_control)]:
446 if bool(option.int_value):
447 control.show()
448 else:
449 control.hide()
452 def show_hide_buttons(self):
453 for i in range(len(self.buttons)):
454 if TOOLOPTIONS.int_value & (1 << i):
455 self.buttons[i].show()
456 else:
457 self.buttons[i].hide()
460 def set_shuffle(self, value):
461 try:
462 self._shuffle = value
463 self.buttons[BTN_SHUFFLE].set_active(self._shuffle)
464 except:
465 pass #this gets called before everything is built
466 shuffle = property(lambda self: self._shuffle, set_shuffle)
469 def set_repeat(self, value):
470 try:
471 self._repeat = value
472 self.buttons[BTN_REPEAT].set_active(self._repeat)
473 except:
474 pass #this gets called before everything is built
475 repeat = property(lambda self: self._repeat, set_repeat)
478 def resize(self, widget, rectangle):
479 """Called when the window resizes."""
480 width = rectangle[2]
481 height = rectangle[3]
482 try:
483 awidth = width
484 self.album_img.get_image() #raises an exception if not valid
485 except:
486 if ALBUM_ART.int_value:
487 awidth = width - ALBUM_COVER_SIZE
489 width = max(width, -1)
490 awidth = max(awidth, -1)
492 if self.display_size != (width, height):
493 self.display_size = (width, height)
494 self.display_box.set_size_request(awidth, -1)
495 self.display_song.set_size_request(awidth, -1)
496 self.display_album.set_size_request(awidth, -1)
497 self.display_artist.set_size_request(awidth, -1)
498 self.display_status.set_size_request(awidth, -1)
501 def set_sensitive(self, state):
502 self.buttons[BTN_PLAYLIST].set_sensitive(state)
503 self.buttons[BTN_PLAY].set_sensitive(state)
504 self.buttons[BTN_NEXT].set_sensitive(state)
505 self.buttons[BTN_STOP].set_sensitive(state)
508 def server(self):
509 """process external/remote commands"""
510 def callback(window, cmd, args):
511 if cmd == 'play':
512 self.play()
513 elif cmd == 'stop':
514 self.stop()
515 elif cmd == 'pause':
516 self.pause()
517 elif cmd == 'next':
518 self.next()
519 elif cmd == 'prev':
520 self.prev()
521 elif cmd == 'add_songs':
522 self.add_songs(args)
523 elif cmd == 'load_songs':
524 self.load_songs(args)
525 else:
526 rox.info("Bad rpc message")
528 xsoap.register_server("_ROX_MUSICBOX")
529 xsoap.register_callback(callback)
532 def update_thd(self, button=None):
533 """load songs from source dirs"""
534 self.load()
537 def refresh(self):
538 self.library = [LIBRARY.value]
539 self.update_thd()
542 def loading(self):
543 self.display_status.set_text(_("Loading")+': '+str(len(self.playlist)))
544 if len(self.playlist):
545 self.set_sensitive(True)
546 else:
547 self.set_sensitive(False)
550 def load(self):
551 """Load the playlist either from a saved xml file, or from source dirs"""
552 self.display_status.set_text(_("Loading songs, please wait..."))
553 self.playlist.get_songs(self.library, self.loading, self.replace_library)
554 self.display_status.set_text(_("Ready")+': '+_("loaded ")+str(len(self.playlist))+_(" songs"))
556 if len(self.playlist):
557 self.set_sensitive(True)
558 else:
559 self.set_sensitive(False)
561 if self.replace_library and len(self.playlist):
562 if self.shuffle:
563 self.next()
564 else:
565 self.play()
568 def save(self):
569 """Save the current list"""
570 # box = saving.SaveBox(self.playlist, rox.choices.save(APP_NAME, 'Library.xml'), 'text/xml')
571 file = 'MyMusic.music'
572 path = os.path.join(rox.basedir.save_config_path(APP_DOMAIN, APP_NAME), file)
573 box = saving.SaveBox(self.playlist, path, 'application/x-music-playlist')
574 box.show()
577 def load_args(self, args, replace=True):
578 """Accept files and folders from the command line (or dropped on our icon)"""
579 self.replace_library = replace
580 if len(args):
581 path = []
582 for s in args:
583 path.append(s)
584 self.library = path
585 self.update_thd()
588 def add_songs(self, args):
589 self.load_args(args, False)
592 def load_songs(self, args):
593 self.load_args(args, True)
596 def play(self):
597 """Play the current song"""
598 size = self.toolbar.get_icon_size()
600 try:
601 self.player.stop()
602 self.current_song = self.playlist.get()
603 self.player.play(self.current_song.filename, self.current_song.type)
604 self.image_play.set_from_stock(gtk.STOCK_MEDIA_PAUSE, size)
605 self.buttons[BTN_PREV].set_sensitive(self.playlist.get_previous())
606 self.display_song.set_text(self.current_song.title)
607 self.display_artist.set_text(self.current_song.artist)
608 self.display_album.set_text(self.current_song.album)
610 tooltips.set_tip(self.buttons[BTN_PLAY], _('Play')+' ['+self.current_song.title+']', tip_private=None)
612 except TypeError, detail:
613 rox.alert(str(detail))
614 except:
615 rox.alert(_("Failed to start playing %s") % self.current_song.filename)
617 if self.playlistUI:
618 self.playlistUI.sync()
620 try:
621 folder = os.path.dirname(self.current_song.filename)
622 pixbuf = None
623 for filename in ['.DirIcon',
624 'Folder.jpg', 'folder.jpg', '.folder.jpg',
625 'Folder.png', 'folder.png', '.folder.png',
626 'Album.jpg', 'album.jpg', '.album.jpg',
627 'Album.png', 'album.png', '.album.png',
628 'Cover.jpg', 'cover.jpg', '.cover.jpg',
629 'Cover.png', 'cover.png', '.cover.png',
631 image = os.path.join(folder, filename)
632 if os.access(image, os.R_OK):
633 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(image, ALBUM_COVER_SIZE, ALBUM_COVER_SIZE)
634 break
635 self.album_img.set_from_pixbuf(pixbuf)
636 except:
637 pass
639 #force a resize because the labels may have changed
640 self.resize(None, [0, 0, 0, 0])
643 def play_pause(self, button=None):
644 """Play button handler (toggle between play and pause)"""
645 if (self.player.state == 'play') or (self.player.state == 'pause'):
646 self.pause()
647 else:
648 self.play()
651 def prev(self, button=None):
652 """Skip to previous song and play it"""
653 self.current_song = self.playlist.prev()
654 self.play()
657 def next(self, button=None):
658 """Skip to next song and play it (with shuffle and repeat)"""
659 if self.shuffle:
660 self.playlist.shuffle()
661 self.current_song = self.playlist.get()
662 else:
663 try:
664 self.current_song = self.playlist.next()
665 except StopIteration:
666 if self.repeat:
667 self.current_song = self.playlist.first()
668 else:
669 self.stop()
670 return True
671 self.play()
674 def stop(self, button=None):
675 """Stop playing"""
676 size = self.toolbar.get_icon_size()
677 self.player.stop()
678 self.current_song = None
679 self.image_play.set_from_stock(gtk.STOCK_MEDIA_PLAY, size)
680 self.seek_bar.set_value(0.0)
683 def pause(self, button=None):
684 """Pause playing (toggle)"""
685 size = self.toolbar.get_icon_size()
686 self.player.pause()
687 if (self.player.state == 'play'):
688 self.image_play.set_from_stock(gtk.STOCK_MEDIA_PAUSE, size)
689 else:
690 self.image_play.set_from_stock(gtk.STOCK_MEDIA_PLAY, size)
693 def display_update(self):
694 duration = int(self.player.remain + self.player.elapse)
695 if duration:
696 progress = float(self.player.elapse)/duration
697 else:
698 progress = 0
700 min = string.zfill(str(int(duration)%3600/60),2)
701 sec = string.zfill(str(int(duration)%3600%60),2)
702 total = min+':'+sec
704 minremain = string.zfill(str(self.player.remain%3600/60),2)
705 secremain = string.zfill(str(self.player.remain%3600%60),2)
706 remain = minremain+':'+secremain
708 minelapse = string.zfill(str(self.player.elapse%3600/60),2)
709 secelapse = string.zfill(str(self.player.elapse%3600%60),2)
710 elapse = minelapse+':'+secelapse
712 show_remain = bool(TIMEDISPLAY.int_value)
713 if show_remain:
714 self.time_string = remain+' / '+total
715 else:
716 self.time_string = elapse+' / '+total
718 state_string = ''
719 if self.player.state == 'play':
720 self.display_status.set_text(_("Playing")+': '+self.time_string)
721 self.seek_bar.handler_block(self.seek_id)
722 self.seek_bar.set_value(progress)
723 self.seek_bar.handler_unblock(self.seek_id)
724 elif self.player.state == 'pause':
725 self.display_status.set_text(_("Paused")+': '+self.time_string)
726 elif self.player.state == 'stop':
727 self.display_status.set_text(_("Stopped"))
728 elif self.player.state == 'eof':
729 self.display_status.set_text("")
730 self.next()
732 if (self.window_state & gtk.gdk.WINDOW_STATE_ICONIFIED):
733 self.set_title(self.current_song.title+' - '+self.time_string)
734 else:
735 tooltips.set_tip(self.seek_bar_control, self.time_string, tip_private=None)
738 #update the volume control if something other than us changed it
739 self.volume.set_value(self.player.get_volume(MIXER_DEVICE.value, MIXER_CHANNEL.value))
741 return True #keep running
744 def delete_event(self, ev, e1):
745 """Same as close, but called from the window manager"""
746 self.close()
749 def window_state_event(self, window, event):
750 """Track changes in window state and such..."""
751 self.my_gdk_window = event.window
752 self.window_state = event.new_window_state
753 if not (self.window_state & gtk.gdk.WINDOW_STATE_ICONIFIED):
754 self.set_title(APP_NAME)
757 def close(self, button = None):
758 """Stop playing, kill the player and exit"""
759 self.stop()
760 if self.playlistUI:
761 self.playlistUI.close()
763 xsoap.unregister_server("_ROX_MUSICBOX")
764 self.destroy()
767 def get_options(self):
768 """Used as the notify callback when options change"""
769 if SHUFFLE.has_changed:
770 self.shuffle = SHUFFLE.int_value
772 if REPEAT.has_changed:
773 self.repeat = REPEAT.int_value
775 if SONG_FONT.has_changed or BASE_FONT.has_changed:
776 self.set_fonts()
778 if FG_COLOR.has_changed or BG_COLOR.has_changed:
779 self.set_colors()
781 if TOOLOPTIONS.has_changed:
782 self.show_hide_buttons()
784 if SH_TOOLBAR.has_changed or SH_VOLUME.has_changed or SH_SEEKBAR.has_changed:
785 self.show_hide_controls()
787 if MINITOOLS.has_changed:
788 if bool(MINITOOLS.int_value):
789 self.toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR)
790 else:
791 self.toolbar.set_icon_size(gtk.ICON_SIZE_LARGE_TOOLBAR)
793 if WORDWRAP.has_changed:
794 self.set_line_wrap()
796 if ALBUM_ART.has_changed:
797 if ALBUM_ART.int_value:
798 self.album_img.show()
799 self.display.set_size_request(ALBUM_COVER_SIZE, ALBUM_COVER_SIZE)
800 else:
801 self.album_img.hide()
802 self.display.set_size_request(-1, -1)
805 def show_options(self, button=None):
806 """Options edit dialog"""
807 rox.edit_options()
810 def show_playlist(self, button=None):
811 """Display the playlist window"""
812 if not self.playlistUI:
813 self.playlistUI = playlistui.PlaylistUI(self.playlist, self)
814 self.playlistUI.connect('destroy', self.playlist_close)
815 self.buttons[BTN_PLAYLIST].set_sensitive(False)
818 def show_dir(self, *dummy):
819 ''' Pops up a filer window containing the current song, or the library location '''
820 if self.current_song:
821 filer.show_file(self.playlist.get().filename)
822 else:
823 filer.show_file(os.path.expanduser(LIBRARY.value))
826 def playlist_close(self, item=None):
827 """Notice when the playlistUI goes away (so we don't crash)"""
828 self.playlistUI = None
829 self.buttons[BTN_PLAYLIST].set_sensitive(True)
832 def button_press(self, text, event):
833 """Popup menu handler"""
834 if event.button != 3:
835 return 0
836 self.menu.popup(self, event)
837 #GTK2.4 self.menu.popup(None, None, None, event.button, 0)
838 return 1
840 def menukey_press(self, widget):
841 ''' Called when the user hits the menu key on their keyboard. '''
842 self.menu.popup(self, None)
844 def get_info(self):
845 InfoWin.infowin(APP_NAME)
848 def adjust_seek_bar(self, pos):
849 """Set the playback position (seek)"""
850 self.player.seek(pos.get_value())
853 def adjust_volume(self, vol):
854 """Set the playback volume"""
855 self.player.set_volume(vol.get_value(), MIXER_DEVICE.value, MIXER_CHANNEL.value)
858 def xds_drag_motion(self, widget, context, x, y, timestamp):
859 pass
862 def xds_drag_drop(self, widget, context, data, info, time):
863 """Check if the Shift key is pressed or not when Dropping files"""
864 if context.actions & gtk.gdk.ACTION_MOVE:
865 self.replace_library = True
866 else:
867 self.replace_library = False
868 return loading.XDSLoader.xds_drag_drop(self, widget, context, data, info, time)
871 def xds_load_uris(self, uris):
872 """Accept files and folders dropped on us as new Library"""
873 path = []
874 #strip off the 'file://' part and concatenate them
875 for s in uris:
876 path.append(rox.get_local_path(s))
877 self.library = path
878 self.update_thd()