4 Copyright 2004 Kenneth Hayber <ken@hayber.us>
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
21 from __future__
import generators
23 import os
, sys
, re
, string
, threading
, pango
, gtk
, gobject
24 from threading
import *
27 from rox
import Menu
, app_options
, loading
, saving
, InfoWin
, OptionsBox
, filer
28 from rox
.options
import Option
32 import player
, playlist
, playlistui
, xsoap
, plugins
34 rox
.report_exception()
37 #Who am I and how did I get here?
40 APP_DOMAIN
= 'hayber.us'
44 #Toolbar button indexes
56 #Bitmaps (potentially) used in this application
57 factory
= gtk
.IconFactory()
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', 'media-repeat', 'media-shuffle',
63 # 'media_track', 'stock_playlist',
64 # 'stock_volume-max', 'stock_volume-med', 'stock_volume-min',
65 # 'stock_volume-mute', 'stock_volume-0'
67 path
= os
.path
.join(rox
.app_dir
, "images", name
+ ".svg")
68 pixbuf
= gtk
.gdk
.pixbuf_new_from_file(path
)
70 print >>sys
.stderr
, "Can't load stock icon '%s'" % name
72 gtk
.stock_add([(name
, name
, 0, 0, "")])
73 factory
.add(name
, gtk
.IconSet(pixbuf
= pixbuf
))
77 #Options.xml processing
78 from rox
import choices
79 choices
.migrate(APP_NAME
, APP_DOMAIN
)
80 rox
.setup_app_options(APP_NAME
, site
=APP_DOMAIN
)
81 Menu
.set_save_name(APP_NAME
, site
=APP_DOMAIN
)
83 #assume that everyone puts their music in ~/Music
84 LIBRARY
= Option('library', os
.path
.expanduser('~')+'/Music')
86 #how to parse each library leaf to get artist, album, title...
87 LIBRARY_RE
= Option('library_re', '^.*/(?P<artist>.*)/(?P<album>.*)/(?P<title>.*)')
89 #the native driver type you want to use ('ao', 'alsa', 'oss', 'linux')
90 DRIVER_TYPE
= Option('driver_type', 'alsa')
91 #the ao driver type you want to use ('esd', 'oss', 'alsa', 'alsa09')
92 DRIVER_ID
= Option('driver_id', 'alsa')
93 SOUND_DEVICE
= Option('sound_device', 'default') # '/dev/mixer' or 'default'
95 MIXER_DEVICE
= Option('mixer_device', 'default') # '/dev/mixer' or 'default'
96 MIXER_CHANNEL
= Option('mixer_channel', 'PCM')
98 SHUFFLE
= Option('shuffle', 0)
99 REPEAT
= Option('repeat', 0)
101 #Don't replay any of the last n songs in shuffle mode
102 SHUFFLE_CACHE_SIZE
= Option('shuffle_cache', 10)
104 #Buffer size used by audio device read/write
105 AUDIO_BUFFER_SIZE
= Option('audio_buffer', 4096)
108 SONG_FONT
= Option('song_font', None)
109 BASE_FONT
= Option('base_font', None)
110 BG_COLOR
= Option('bg_color', '#A6A699')
111 FG_COLOR
= Option('fg_color', '#000000')
114 SH_TOOLBAR
= Option('toolbar', True)
115 SH_VOLUME
= Option('volume', True)
116 SH_SEEKBAR
= Option('seekbar', True)
119 WORDWRAP
= Option('word_wrap', False)
120 MINITOOLS
= Option('mini_toolbar', False)
121 TIMEDISPLAY
= Option('time_display', False)
122 ALBUM_ART
= Option('album_art', True)
124 tooltips
= gtk
.Tooltips()
127 def build_tool_options(box
, node
, label
, option
):
128 """Custom Option widget to allow show/hide each toolbar button"""
130 toolbar
= gtk
.Toolbar()
131 toolbar
.set_style(gtk
.TOOLBAR_ICONS
)
134 def item_changed(thing
):
135 if done
: #supress widget updates until later
136 box
.check_widget(option
)
138 type1
= gtk
.TOOLBAR_CHILD_TOGGLEBUTTON
140 #(type, text, icon, callback)
141 (type1
, _("Close"), gtk
.STOCK_CLOSE
, item_changed
),
142 (type1
, _("Prev"), gtk
.STOCK_MEDIA_PREVIOUS
, item_changed
),
143 (type1
, _("Play"), gtk
.STOCK_MEDIA_PLAY
, item_changed
),
144 (type1
, _("Stop"), gtk
.STOCK_MEDIA_STOP
, item_changed
),
145 (type1
, _("Next"), gtk
.STOCK_MEDIA_NEXT
, item_changed
),
146 (type1
, _("Repeat"), 'media-repeat', item_changed
),
147 (type1
, _("Shuffle"), 'media-shuffle', item_changed
),
148 (type1
, _("Playlist"), gtk
.STOCK_INDEX
, item_changed
),
149 (type1
, _("Options"), gtk
.STOCK_PREFERENCES
, item_changed
),
153 for (type, text
, icon
, callback
) in buttons
:
154 image
= gtk
.image_new_from_stock(icon
, gtk
.ICON_SIZE_LARGE_TOOLBAR
)
155 controls
.append(toolbar
.insert_element(type, None, text
, text
,
156 None, image
, callback
, None, -1))
158 #build a bitmask of which buttons are active
161 for i
in range(len(controls
)):
162 if controls
[i
].get_active():
165 def set_values(): pass
166 box
.handlers
[option
] = (get_values
, set_values
)
168 #initialize the button states
169 for i
in range(len(controls
)):
170 if option
.int_value
& (1 << i
):
171 controls
[i
].set_active(True)
172 done
= True #OK, updates activated.
174 box
.may_add_tip(toolbar
, node
)
177 OptionsBox
.widget_registry
['tool_options'] = build_tool_options
178 TOOLOPTIONS
= Option('toolbar_disable', -1)
180 rox
.app_options
.notify()
183 class MusicBox(rox
.Window
, loading
.XDSLoader
):
184 """A Music Player for mp3 and ogg - main class"""
191 """Constructor for MusicBox"""
192 rox
.Window
.__init
__(self
)
193 loading
.XDSLoader
.__init
__(self
, plugins
.TYPE_LIST
)
195 # Main window settings
196 self
.set_title(APP_NAME
)
197 self
.set_role("MainWindow")
200 rox
.app_options
.add_notify(self
.get_options
)
201 self
.connect('delete_event', self
.delete_event
)
202 self
.connect('window-state-event', self
.window_state_event
)
203 self
.connect('drag-motion', self
.xds_drag_motion
)
206 self
.replace_library
= False
207 self
.library
= LIBRARY
.value
.split(':')
209 self
.playlistUI
= None
210 self
.current_song
= None
212 self
.shuffle
= bool(SHUFFLE
.int_value
)
213 self
.repeat
= bool(REPEAT
.int_value
)
215 # Build and Init everything
216 #GTK2.4 self.uimanager = gtk.UIManager()
217 #GTK2.4 self.uimanager.insert_action_group(self.build_actions(), 0)
218 #GTK2.4 self.uimanager.add_ui_from_file('ui.xml')
219 #GTK2.4 self.menu = self.uimanager.get_widget('/ui/popup')
220 #GTK2.4 self.toolbar = self.uimanager.get_widget('/ui/toolbar')
221 #GTK2.4 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
222 #GTK2.4 self.connect('button-press-event', self.button_press)
223 #GTK2.4 self.connect('popup-menu', self.menukey_press)
233 # Pack and show widgets
234 self
.vbox
= gtk
.VBox()
235 self
.hbox
= gtk
.HBox()
237 self
.vbox
.add(self
.hbox
)
239 self
.hbox
.pack_start(self
.display
, True, True, 0)
240 self
.hbox
.pack_end(self
.volume_control
, False, True, 0)
241 self
.vbox
.pack_end(self
.toolbar
, False, True, 0)
242 self
.vbox
.pack_end(self
.seek_bar_control
, False, True, 0)
245 self
.show_hide_controls()
246 self
.show_hide_buttons()
248 if not ALBUM_ART
.int_value
:
249 self
.album_img
.hide()
251 self
.display
.set_size_request(ALBUM_COVER_SIZE
, ALBUM_COVER_SIZE
)
258 self
.playlist
= playlist
.Playlist(SHUFFLE_CACHE_SIZE
.int_value
, LIBRARY_RE
.value
)
259 self
.player
= player
.Player(DRIVER_TYPE
.value
, DRIVER_ID
.value
, AUDIO_BUFFER_SIZE
.int_value
, SOUND_DEVICE
.value
)
260 self
.foo
= Thread(name
='player', target
=self
.player
.run
)
261 self
.foo
.setDaemon(True)
263 self
.volume
.set_value(self
.player
.get_volume(MIXER_DEVICE
.value
, MIXER_CHANNEL
.value
))
265 if len(sys
.argv
) > 1:
266 self
.load_args(sys
.argv
[1:], True)
268 self
.load_args([], False)
270 gobject
.timeout_add(500, self
.display_update
)
272 #GTK2.4 def build_actions(self):
273 #GTK2.4 actions = gtk.ActionGroup('main')
274 #GTK2.4 actions.add_action(gtk.Action('quit', _("Quit"), _("Quit the application"), gtk.STOCK_QUIT))
275 #GTK2.4 actions.add_action(gtk.Action('close', _("Close"), _("Close this window"), gtk.STOCK_CLOSE))
277 #GTK2.4 actions.add_action(gtk.Action('options', _("Options"), _("Edit Options"), gtk.STOCK_PREFERENCES))
278 #GTK2.4 actions.add_action(gtk.Action('info', _("Info"), _("Show program info"), gtk.STOCK_DIALOG_INFO))
280 #GTK2.4 actions.add_action(gtk.Action('play', _("Play"), _("Play"), 'media-play'))
281 #GTK2.4 actions.add_action(gtk.Action('stop', _("Stop"), _("Stop"), 'media-stop'))
282 #GTK2.4 actions.add_action(gtk.Action('prev', _("Prev"), _("Prev"), 'media-prev'))
283 #GTK2.4 actions.add_action(gtk.Action('next', _("Next"), _("Next"), 'media-next'))
285 #GTK2.4 actions.add_action(gtk.Action('playlist', _("Playlist"), _("Show Playlist"), gtk.STOCK_INDEX))
286 #GTK2.4 actions.add_action(gtk.Action('open', _("Open"), _("Open Location"), gtk.STOCK_GO_UP))
287 #GTK2.4 actions.add_action(gtk.Action('save', _("Save"), _("Save Playlist"), gtk.STOCK_SAVE))
289 #GTK2.4 actions.add_action(gtk.Action('shuffle', _("Shuffle"), _("Shuffle"), 'media-shuffle'))
290 #GTK2.4 actions.add_action(gtk.Action('repeat', _("Repeat"), _("Repeat"), 'media-repeat'))
292 #GTK2.4 actions.add_action(gtk.Action('none', None, None, 0))
293 #GTK2.4 return actions
296 def build_menu(self
):
297 self
.add_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
298 self
.connect('button-press-event', self
.button_press
)
299 self
.connect('popup-menu', self
.menukey_press
)
300 self
.menu
= Menu
.Menu('main', [
301 Menu
.Action(_("Play")+'\/'+_("Pause"), 'play_pause', '', gtk
.STOCK_MEDIA_PLAY
),
302 Menu
.Action(_("Stop"), 'stop', '', gtk
.STOCK_MEDIA_STOP
),
305 Menu
.Action(_("Back"), 'prev', '', gtk
.STOCK_MEDIA_PREVIOUS
),
306 Menu
.Action(_("Next"), 'next', '', gtk
.STOCK_MEDIA_NEXT
),
309 Menu
.SubMenu(_('Playlist'), [
310 Menu
.Action(_("Show"), 'show_playlist', '', gtk
.STOCK_INDEX
),
311 Menu
.Action(_("Open location"), 'show_dir', '', gtk
.STOCK_GO_UP
),
312 Menu
.Action(_("Save"), 'save', '', gtk
.STOCK_SAVE
),
313 Menu
.ToggleItem(_("Shuffle"), 'shuffle'),
314 Menu
.ToggleItem(_("Repeat"), 'repeat'),
318 Menu
.Action(_("Options"), 'show_options', '', gtk
.STOCK_PREFERENCES
),
319 Menu
.Action(_('Info'), 'get_info', '', gtk
.STOCK_DIALOG_INFO
),
322 Menu
.Action(_("Quit"), 'close', '', gtk
.STOCK_CLOSE
),
324 self
.menu
.attach(self
,self
)
327 def build_toolbar(self
):
328 self
.toolbar
= gtk
.Toolbar()
329 self
.toolbar
.set_style(gtk
.TOOLBAR_ICONS
)
330 if bool(MINITOOLS
.int_value
):
331 self
.toolbar
.set_icon_size(gtk
.ICON_SIZE_SMALL_TOOLBAR
)
333 self
.toolbar
.set_icon_size(gtk
.ICON_SIZE_LARGE_TOOLBAR
)
335 type1
= gtk
.TOOLBAR_CHILD_BUTTON
336 type2
= gtk
.TOOLBAR_CHILD_TOGGLEBUTTON
339 #(type, text, icon, callback)
340 (type1
, _("Close"), gtk
.STOCK_CLOSE
, self
.close
),
341 (type1
, _("Prev"), gtk
.STOCK_MEDIA_PREVIOUS
, self
.prev
),
342 (type1
, _("Play"), gtk
.STOCK_MEDIA_PLAY
, self
.play_pause
),
343 (type1
, _("Stop"), gtk
.STOCK_MEDIA_STOP
, self
.stop
),
344 (type1
, _("Next"), gtk
.STOCK_MEDIA_NEXT
, self
.next
),
345 (type2
, _("Repeat"), 'media-repeat', lambda b
: self
.set_repeat(b
.get_active())),
346 (type2
, _("Shuffle"), 'media-shuffle', lambda b
: self
.set_shuffle(b
.get_active())),
347 (type1
, _("Playlist"), gtk
.STOCK_INDEX
, self
.show_playlist
),
348 (type1
, _("Options"), gtk
.STOCK_PREFERENCES
, self
.show_options
),
352 for (type, text
, icon
, callback
) in items
:
353 image
= gtk
.image_new_from_stock(icon
, self
.toolbar
.get_icon_size())
354 buttons
.append(self
.toolbar
.insert_element(type, None, text
, text
,
355 None, image
, callback
, None, -1))
356 if text
== _("Play"): #this one changes later, so we have to save it
357 self
.image_play
= image
359 self
.buttons
= buttons
361 buttons
[BTN_REPEAT
].set_active(self
.repeat
)
362 buttons
[BTN_SHUFFLE
].set_active(self
.shuffle
)
363 buttons
[BTN_PLAYLIST
].set_sensitive(False)
366 def build_labels(self
):
367 self
.display
= gtk
.Layout()
368 self
.display_size
= (0, 0)
369 self
.display
.connect('size-allocate', self
.resize
)
371 self
.album_img
= gtk
.Image()
372 self
.album_img
.set_alignment(0.0, 0.0)
374 hbox
= gtk
.HBox(False, 5)
376 hbox
.pack_start(self
.album_img
, False, False, 0)
377 hbox
.pack_end(vbox
, True, True, 0)
378 self
.display
.put(hbox
, 0, 0)
379 self
.display_box
= vbox
381 self
.display_song
= gtk
.Label()
382 self
.display_song
.set_alignment(0.0, 0.0)
384 self
.display_album
= gtk
.Label()
385 self
.display_album
.set_alignment(0.0, 0.0)
387 self
.display_artist
= gtk
.Label()
388 self
.display_artist
.set_alignment(0.0, 0.0)
390 self
.display_status
= gtk
.Label()
391 self
.display_status
.set_alignment(0.0, 0.0)
393 vbox
.pack_start(self
.display_song
, False, True, 0)
394 vbox
.pack_start(self
.display_album
, False, True, 0)
395 vbox
.pack_start(self
.display_artist
, False, True, 0)
396 vbox
.pack_start(self
.display_status
, False, True, 0)
399 def set_line_wrap(self
):
400 self
.display_song
.set_line_wrap(bool(WORDWRAP
.int_value
))
401 self
.display_album
.set_line_wrap(bool(WORDWRAP
.int_value
))
402 self
.display_artist
.set_line_wrap(bool(WORDWRAP
.int_value
))
403 self
.display_status
.set_line_wrap(bool(WORDWRAP
.int_value
))
406 def build_misc(self
):
407 self
.volume
= gtk
.Adjustment(50.0, 0.0, 100.0, 1.0, 10.0, 0.0)
408 self
.volume
.connect('value_changed', self
.adjust_volume
)
409 self
.volume_control
= gtk
.VScale(self
.volume
)
410 self
.volume_control
.set_draw_value(False)
411 self
.volume_control
.set_inverted(True)
412 self
.volume_control
.set_size_request(-1, 90)
414 self
.seek_bar
= gtk
.Adjustment(0.0, 0.0, 1.0, 0.01, 0.1, 0.0)
415 self
.seek_id
= self
.seek_bar
.connect('value_changed', self
.adjust_seek_bar
)
416 self
.seek_bar_control
= gtk
.HScale(self
.seek_bar
)
417 self
.seek_bar_control
.set_update_policy(gtk
.UPDATE_DELAYED
)
418 self
.seek_bar_control
.set_draw_value(False)
419 self
.seek_bar_control
.set_size_request(100, -1)
423 song_font
= pango
.FontDescription(SONG_FONT
.value
)
424 base_font
= pango
.FontDescription(BASE_FONT
.value
)
425 self
.display_song
.modify_font(song_font
)
426 self
.display_album
.modify_font(base_font
)
427 self
.display_artist
.modify_font(base_font
)
428 self
.display_status
.modify_font(base_font
)
431 def set_colors(self
):
432 fg_color
= gtk
.gdk
.color_parse(FG_COLOR
.value
)
433 bg_color
= gtk
.gdk
.color_parse(BG_COLOR
.value
)
434 self
.display
.modify_bg(gtk
.STATE_NORMAL
, bg_color
)
435 self
.display_song
.modify_fg(gtk
.STATE_NORMAL
, fg_color
)
436 self
.display_album
.modify_fg(gtk
.STATE_NORMAL
, fg_color
)
437 self
.display_artist
.modify_fg(gtk
.STATE_NORMAL
, fg_color
)
438 self
.display_status
.modify_fg(gtk
.STATE_NORMAL
, fg_color
)
441 def show_hide_controls(self
):
442 for (option
, control
) in [
443 (SH_VOLUME
, self
.volume_control
), (SH_TOOLBAR
, self
.toolbar
),
444 (SH_SEEKBAR
, self
.seek_bar_control
)]:
445 if bool(option
.int_value
):
451 def show_hide_buttons(self
):
452 for i
in range(len(self
.buttons
)):
453 if TOOLOPTIONS
.int_value
& (1 << i
):
454 self
.buttons
[i
].show()
456 self
.buttons
[i
].hide()
459 def set_shuffle(self
, value
):
461 self
._shuffle
= value
462 self
.buttons
[BTN_SHUFFLE
].set_active(self
._shuffle
)
464 pass #this gets called before everything is built
465 shuffle
= property(lambda self
: self
._shuffle
, set_shuffle
)
468 def set_repeat(self
, value
):
471 self
.buttons
[BTN_REPEAT
].set_active(self
._repeat
)
473 pass #this gets called before everything is built
474 repeat
= property(lambda self
: self
._repeat
, set_repeat
)
477 def resize(self
, widget
, rectangle
):
478 """Called when the window resizes."""
480 height
= rectangle
[3]
483 self
.album_img
.get_image() #raises an exception if not valid
485 if ALBUM_ART
.int_value
:
486 awidth
= width
- ALBUM_COVER_SIZE
488 width
= max(width
, -1)
489 awidth
= max(awidth
, -1)
491 if self
.display_size
!= (width
, height
):
492 self
.display_size
= (width
, height
)
493 self
.display_box
.set_size_request(awidth
, -1)
494 self
.display_song
.set_size_request(awidth
, -1)
495 self
.display_album
.set_size_request(awidth
, -1)
496 self
.display_artist
.set_size_request(awidth
, -1)
497 self
.display_status
.set_size_request(awidth
, -1)
500 def set_sensitive(self
, state
):
501 self
.buttons
[BTN_PLAYLIST
].set_sensitive(state
)
502 self
.buttons
[BTN_PLAY
].set_sensitive(state
)
503 self
.buttons
[BTN_NEXT
].set_sensitive(state
)
504 self
.buttons
[BTN_STOP
].set_sensitive(state
)
508 """process external/remote commands"""
509 def callback(window
, cmd
, args
):
520 elif cmd
== 'add_songs':
522 elif cmd
== 'load_songs':
523 self
.load_songs(args
)
525 rox
.info("Bad rpc message")
527 xsoap
.register_server("_ROX_MUSICBOX")
528 xsoap
.register_callback(callback
)
531 def update_thd(self
, button
=None):
532 """load songs from source dirs"""
537 self
.library
= [LIBRARY
.value
]
542 self
.display_status
.set_text(_("Loading")+': '+str(len(self
.playlist
)))
543 if len(self
.playlist
):
544 self
.set_sensitive(True)
546 self
.set_sensitive(False)
550 """Load the playlist either from a saved xml file, or from source dirs"""
551 self
.display_status
.set_text(_("Loading songs, please wait..."))
552 self
.playlist
.get_songs(self
.library
, self
.loading
, self
.replace_library
)
553 self
.display_status
.set_text(_("Ready")+': '+_("loaded ")+str(len(self
.playlist
))+_(" songs"))
555 if len(self
.playlist
):
556 self
.set_sensitive(True)
558 self
.set_sensitive(False)
560 if self
.replace_library
and len(self
.playlist
):
568 """Save the current list"""
569 # box = saving.SaveBox(self.playlist, rox.choices.save(APP_NAME, 'Library.xml'), 'text/xml')
570 file = 'MyMusic.music'
571 path
= os
.path
.join(rox
.basedir
.save_config_path(APP_DOMAIN
, APP_NAME
), file)
572 box
= saving
.SaveBox(self
.playlist
, path
, 'application/x-music-playlist')
576 def load_args(self
, args
, replace
=True):
577 """Accept files and folders from the command line (or dropped on our icon)"""
578 self
.replace_library
= replace
587 def add_songs(self
, args
):
588 self
.load_args(args
, False)
591 def load_songs(self
, args
):
592 self
.load_args(args
, True)
596 """Play the current song"""
597 size
= self
.toolbar
.get_icon_size()
601 self
.current_song
= self
.playlist
.get()
602 self
.player
.play(self
.current_song
.filename
, self
.current_song
.type)
603 self
.image_play
.set_from_stock(gtk
.STOCK_MEDIA_PAUSE
, size
)
604 self
.buttons
[BTN_PREV
].set_sensitive(self
.playlist
.get_previous())
605 self
.display_song
.set_text(self
.current_song
.title
)
606 self
.display_artist
.set_text(self
.current_song
.artist
)
607 self
.display_album
.set_text(self
.current_song
.album
)
609 tooltips
.set_tip(self
.buttons
[BTN_PLAY
], _('Play')+' ['+self
.current_song
.title
+']', tip_private
=None)
611 except TypeError, detail
:
612 rox
.alert(str(detail
))
614 rox
.alert(_("Failed to start playing %s") % self
.current_song
.filename
)
617 self
.playlistUI
.sync()
620 folder
= os
.path
.dirname(self
.current_song
.filename
)
622 for filename
in ['.DirIcon',
623 'Folder.jpg', 'folder.jpg', '.folder.jpg',
624 'Folder.png', 'folder.png', '.folder.png',
625 'Album.jpg', 'album.jpg', '.album.jpg',
626 'Album.png', 'album.png', '.album.png',
627 'Cover.jpg', 'cover.jpg', '.cover.jpg',
628 'Cover.png', 'cover.png', '.cover.png',
630 image
= os
.path
.join(folder
, filename
)
631 if os
.access(image
, os
.R_OK
):
632 pixbuf
= gtk
.gdk
.pixbuf_new_from_file_at_size(image
, ALBUM_COVER_SIZE
, ALBUM_COVER_SIZE
)
634 self
.album_img
.set_from_pixbuf(pixbuf
)
638 #force a resize because the labels may have changed
639 self
.resize(None, [0, 0, 0, 0])
642 def play_pause(self
, button
=None):
643 """Play button handler (toggle between play and pause)"""
644 if (self
.player
.state
== 'play') or (self
.player
.state
== 'pause'):
650 def prev(self
, button
=None):
651 """Skip to previous song and play it"""
652 self
.current_song
= self
.playlist
.prev()
656 def next(self
, button
=None):
657 """Skip to next song and play it (with shuffle and repeat)"""
659 self
.playlist
.shuffle()
660 self
.current_song
= self
.playlist
.get()
663 self
.current_song
= self
.playlist
.next()
664 except StopIteration:
666 self
.current_song
= self
.playlist
.first()
673 def stop(self
, button
=None):
675 size
= self
.toolbar
.get_icon_size()
677 self
.current_song
= None
678 self
.image_play
.set_from_stock(gtk
.STOCK_MEDIA_PLAY
, size
)
679 self
.seek_bar
.set_value(0.0)
682 def pause(self
, button
=None):
683 """Pause playing (toggle)"""
684 size
= self
.toolbar
.get_icon_size()
686 if (self
.player
.state
== 'play'):
687 self
.image_play
.set_from_stock(gtk
.STOCK_MEDIA_PAUSE
, size
)
689 self
.image_play
.set_from_stock(gtk
.STOCK_MEDIA_PLAY
, size
)
692 def display_update(self
):
693 duration
= int(self
.player
.remain
+ self
.player
.elapse
)
695 progress
= float(self
.player
.elapse
)/duration
699 min = string
.zfill(str(int(duration
)%3600/60),2)
700 sec
= string
.zfill(str(int(duration
)%3600%60),2)
703 minremain
= string
.zfill(str(self
.player
.remain
%3600/60),2)
704 secremain
= string
.zfill(str(self
.player
.remain
%3600%60),2)
705 remain
= minremain
+':'+secremain
707 minelapse
= string
.zfill(str(self
.player
.elapse
%3600/60),2)
708 secelapse
= string
.zfill(str(self
.player
.elapse
%3600%60),2)
709 elapse
= minelapse
+':'+secelapse
711 show_remain
= bool(TIMEDISPLAY
.int_value
)
713 self
.time_string
= remain
+' / '+total
715 self
.time_string
= elapse
+' / '+total
718 if self
.player
.state
== 'play':
719 self
.display_status
.set_text(_("Playing")+': '+self
.time_string
)
720 self
.seek_bar
.handler_block(self
.seek_id
)
721 self
.seek_bar
.set_value(progress
)
722 self
.seek_bar
.handler_unblock(self
.seek_id
)
723 elif self
.player
.state
== 'pause':
724 self
.display_status
.set_text(_("Paused")+': '+self
.time_string
)
725 elif self
.player
.state
== 'stop':
726 self
.display_status
.set_text(_("Stopped"))
727 elif self
.player
.state
== 'eof':
728 self
.display_status
.set_text("")
731 if (self
.window_state
& gtk
.gdk
.WINDOW_STATE_ICONIFIED
):
732 self
.set_title(self
.current_song
.title
+' - '+self
.time_string
)
734 tooltips
.set_tip(self
.seek_bar_control
, self
.time_string
, tip_private
=None)
737 #update the volume control if something other than us changed it
738 self
.volume
.set_value(self
.player
.get_volume(MIXER_DEVICE
.value
, MIXER_CHANNEL
.value
))
740 return True #keep running
743 def delete_event(self
, ev
, e1
):
744 """Same as close, but called from the window manager"""
748 def window_state_event(self
, window
, event
):
749 """Track changes in window state and such..."""
750 self
.my_gdk_window
= event
.window
751 self
.window_state
= event
.new_window_state
752 if not (self
.window_state
& gtk
.gdk
.WINDOW_STATE_ICONIFIED
):
753 self
.set_title(APP_NAME
)
756 def close(self
, button
= None):
757 """Stop playing, kill the player and exit"""
760 self
.playlistUI
.close()
762 xsoap
.unregister_server("_ROX_MUSICBOX")
766 def get_options(self
):
767 """Used as the notify callback when options change"""
768 if SHUFFLE
.has_changed
:
769 self
.shuffle
= SHUFFLE
.int_value
771 if REPEAT
.has_changed
:
772 self
.repeat
= REPEAT
.int_value
774 if SONG_FONT
.has_changed
or BASE_FONT
.has_changed
:
777 if FG_COLOR
.has_changed
or BG_COLOR
.has_changed
:
780 if TOOLOPTIONS
.has_changed
:
781 self
.show_hide_buttons()
783 if SH_TOOLBAR
.has_changed
or SH_VOLUME
.has_changed
or SH_SEEKBAR
.has_changed
:
784 self
.show_hide_controls()
786 if MINITOOLS
.has_changed
:
787 if bool(MINITOOLS
.int_value
):
788 self
.toolbar
.set_icon_size(gtk
.ICON_SIZE_SMALL_TOOLBAR
)
790 self
.toolbar
.set_icon_size(gtk
.ICON_SIZE_LARGE_TOOLBAR
)
792 if WORDWRAP
.has_changed
:
795 if ALBUM_ART
.has_changed
:
796 if ALBUM_ART
.int_value
:
797 self
.album_img
.show()
798 self
.display
.set_size_request(ALBUM_COVER_SIZE
, ALBUM_COVER_SIZE
)
800 self
.album_img
.hide()
801 self
.display
.set_size_request(-1, -1)
804 def show_options(self
, button
=None):
805 """Options edit dialog"""
809 def show_playlist(self
, button
=None):
810 """Display the playlist window"""
811 if not self
.playlistUI
:
812 self
.playlistUI
= playlistui
.PlaylistUI(self
.playlist
, self
)
813 self
.playlistUI
.connect('destroy', self
.playlist_close
)
814 self
.buttons
[BTN_PLAYLIST
].set_sensitive(False)
817 def show_dir(self
, *dummy
):
818 ''' Pops up a filer window containing the current song, or the library location '''
819 if self
.current_song
:
820 filer
.show_file(self
.playlist
.get().filename
)
822 filer
.show_file(os
.path
.expanduser(LIBRARY
.value
))
825 def playlist_close(self
, item
=None):
826 """Notice when the playlistUI goes away (so we don't crash)"""
827 self
.playlistUI
= None
828 self
.buttons
[BTN_PLAYLIST
].set_sensitive(True)
831 def button_press(self
, text
, event
):
832 """Popup menu handler"""
833 if event
.button
!= 3:
835 self
.menu
.popup(self
, event
)
836 #GTK2.4 self.menu.popup(None, None, None, event.button, 0)
839 def menukey_press(self
, widget
):
840 ''' Called when the user hits the menu key on their keyboard. '''
841 self
.menu
.popup(self
, None)
844 InfoWin
.infowin(APP_NAME
)
847 def adjust_seek_bar(self
, pos
):
848 """Set the playback position (seek)"""
849 self
.player
.seek(pos
.get_value())
852 def adjust_volume(self
, vol
):
853 """Set the playback volume"""
854 self
.player
.set_volume(vol
.get_value(), MIXER_DEVICE
.value
, MIXER_CHANNEL
.value
)
857 def xds_drag_motion(self
, widget
, context
, x
, y
, timestamp
):
861 def xds_drag_drop(self
, widget
, context
, data
, info
, time
):
862 """Check if the Shift key is pressed or not when Dropping files"""
863 if context
.actions
& gtk
.gdk
.ACTION_MOVE
:
864 self
.replace_library
= True
866 self
.replace_library
= False
867 return loading
.XDSLoader
.xds_drag_drop(self
, widget
, context
, data
, info
, time
)
870 def xds_load_uris(self
, uris
):
871 """Accept files and folders dropped on us as new Library"""
873 #strip off the 'file://' part and concatenate them
875 path
.append(rox
.get_local_path(s
))