3 GUI front-end to cdda2wav and lame.
5 Copyright 2004 Kenneth Hayber <khayber@socal.rr.com>
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License.
12 This program is distributed in the hope that it will be useful
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 import gtk
, os
, sys
, signal
, re
, string
, socket
, time
, popen2
, threading
, Queue
24 from random
import Random
25 from threading
import *
28 from rox
import i18n
, app_options
, Menu
, filer
29 from rox
.options
import Option
31 import PyCDDB
, cd_logic
, CDROM
, genres
39 _
= rox
.i18n
.translation(os
.path
.join(rox
.app_dir
, 'Messages'))
41 #Who am I and how did I get here?
42 APP_NAME
= 'Ripper' #I could call it Mr. Giles, but that would be gay.
43 APP_PATH
= os
.path
.split(os
.path
.abspath(sys
.argv
[0]))[0]
46 #Options.xml processing
47 rox
.setup_app_options(APP_NAME
)
49 #assume that everyone puts their music in ~/Music
50 LIBRARY
= Option('library', '~/Music')
53 RIPPER
= Option('ripper', 'cdda2wav')
54 RIPPER_DEV
= Option('ripper_dev', '/dev/cdrom')
55 RIPPER_LUN
= Option('ripper_lun', 'ATAPI:0,1,0')
56 RIPPER_OPTS
= Option('ripper_opts', '-x -H')
58 EJECT_AFTER_RIP
= Option('eject_after_rip', '0')
61 ENCODER
= Option('encoder', 'MP3')
63 MP3_ENCODER
= Option('mp3_encoder', 'lame')
64 MP3_ENCODER_OPTS
= Option('mp3_encoder_opts', '--vbr-new -b160 --nohist --add-id3v2')
66 OGG_ENCODER
= Option('ogg_encoder', 'oggenc')
67 OGG_ENCODER_OPTS
= Option('ogg_encoder_opts', '-q5')
69 #CDDB Server and Options
70 CDDB_SERVER
= Option('cddb_server', 'http://freedb.freedb.org/~cddb/cddb.cgi')
72 rox
.app_options
.notify()
82 # My gentoo python doesn't have universal line ending support compiled in
83 # and these guys (cdda2wav and lame) use CR line endings to pretty-up their output.
85 '''Return a line of input using \r or \n as terminators'''
87 while '\n' not in line
and '\r' not in line
:
89 if char
== '': return line
95 '''Return the full path of an executable if found on the path'''
96 if (filename
== None) or (filename
== ''):
99 env_path
= os
.getenv('PATH').split(':')
101 if os
.access(p
+'/'+filename
, os
.X_OK
):
102 return p
+'/'+filename
106 def strip_illegal(instr
):
107 '''remove illegal (filename) characters from string'''
110 str = string
.translate(str, string
.maketrans(r
'/+{}*.?', r
'--()___'))
114 class Ripper(rox
.Window
):
115 '''Rip and Encode a CD'''
117 rox
.Window
.__init
__(self
)
119 self
.set_title(APP_NAME
)
120 self
.set_default_size(450, 500)
121 self
.set_position(gtk
.WIN_POS_MOUSE
)
123 #capture wm delete event
124 self
.connect("delete_event", self
.delete_event
)
126 # Update things when options change
127 rox
.app_options
.add_notify(self
.get_options
)
131 #######################################
132 swin
= gtk
.ScrolledWindow()
133 self
.scroll_window
= swin
134 swin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
135 swin
.set_shadow_type(gtk
.SHADOW_IN
)
137 self
.store
= gtk
.ListStore(int, str, str, str)
138 view
= gtk
.TreeView(self
.store
)
141 view
.set_rules_hint(True)
143 cell
= gtk
.CellRendererToggle()
144 cell
.connect('toggled', self
.toggle_check
)
145 column
= gtk
.TreeViewColumn('', cell
, active
=COL_ENABLE
)
146 view
.append_column(column
)
147 column
.set_resizable(False)
148 column
.set_reorderable(False)
150 cell
= gtk
.CellRendererText()
151 column
= gtk
.TreeViewColumn(_('Track'), cell
, text
= COL_TRACK
)
152 view
.append_column(column
)
153 column
.set_resizable(True)
154 #column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
155 column
.set_reorderable(False)
157 cell
= gtk
.CellRendererText()
158 column
= gtk
.TreeViewColumn(_('Time'), cell
, text
= COL_TIME
)
159 view
.append_column(column
)
160 column
.set_resizable(True)
161 column
.set_reorderable(False)
163 cell
= gtk
.CellRendererText()
164 column
= gtk
.TreeViewColumn(_('Status'), cell
, text
= COL_STATUS
)
165 view
.append_column(column
)
166 column
.set_resizable(True)
167 column
.set_reorderable(False)
169 view
.connect('row-activated', self
.activate
)
170 self
.selection
= view
.get_selection()
171 self
.handler
= self
.selection
.connect('changed', self
.set_selection
)
174 self
.toolbar
= gtk
.Toolbar()
175 self
.toolbar
.set_style(gtk
.TOOLBAR_ICONS
)
176 self
.toolbar
.insert_stock(gtk
.STOCK_PREFERENCES
,
177 _('Settings'), None, self
.show_options
, None, 0)
178 self
.stop_btn
= self
.toolbar
.insert_stock(gtk
.STOCK_STOP
,
179 _('Stop'), None, self
.stop
, None, 0)
180 self
.rip_btn
= self
.toolbar
.insert_stock(gtk
.STOCK_EXECUTE
,
181 _('Rip & Encode'), None, self
.rip_n_encode
, None, 0)
182 self
.refresh_btn
= self
.toolbar
.insert_stock(gtk
.STOCK_REFRESH
,
183 _('Reload CD'), None, self
.do_get_tracks
, None, 0)
185 self
.toolbar
.insert_stock(gtk
.STOCK_GO_UP
,
186 _('Show destination dir'), None, self
.show_dir
, None, 0)
188 self
.toolbar
.insert_stock(gtk
.STOCK_CLOSE
,
189 _('Close'), None, self
.close
, None, 0)
192 self
.table
= gtk
.Table(5, 2, False)
196 self
.artist_entry
= gtk
.Entry(max=255)
197 self
.artist_entry
.connect('changed', self
.stuff_changed
)
198 self
.table
.attach(gtk
.Label(str=_('Artist')), 0, 1, 2, 3, 0, 0, 4, y_pad
)
199 self
.table
.attach(self
.artist_entry
, 1, 2, 2, 3, gtk
.EXPAND|gtk
.FILL
, 0, x_pad
, y_pad
)
201 self
.album_entry
= gtk
.Entry(max=255)
202 self
.album_entry
.connect('changed', self
.stuff_changed
)
203 self
.table
.attach(gtk
.Label(str=_('Album')), 0, 1, 3, 4, 0, 0, 4, y_pad
)
204 self
.table
.attach(self
.album_entry
, 1, 2, 3, 4, gtk
.EXPAND|gtk
.FILL
, 0, x_pad
, y_pad
)
206 genres
.genre_list
.sort()
207 self
.genre_combo
= gtk
.Combo()
208 self
.genre_combo
.set_popdown_strings(genres
.genre_list
)
209 self
.genre_combo
.entry
.connect('changed', self
.stuff_changed
)
210 self
.table
.attach(gtk
.Label(str=_('Genre')), 0, 1, 4, 5, 0, 0, 4, y_pad
)
211 self
.table
.attach(self
.genre_combo
, 1, 2, 4, 5, gtk
.EXPAND|gtk
.FILL
, 0, x_pad
, y_pad
)
213 self
.year_entry
= gtk
.Entry(max=4)
214 self
.year_entry
.connect('changed', self
.stuff_changed
)
215 self
.table
.attach(gtk
.Label(str=_('Year')), 0, 1, 5, 6, 0, 0, 4, y_pad
)
216 self
.table
.attach(self
.year_entry
, 1, 2, 5, 6, gtk
.EXPAND|gtk
.FILL
, 0, x_pad
, y_pad
)
219 # Create layout, pack and show widgets
220 self
.vbox
= gtk
.VBox()
222 self
.vbox
.pack_start(self
.toolbar
, False, True, 0)
223 self
.vbox
.pack_start(self
.table
, False, True, 0)
224 self
.vbox
.pack_start(self
.scroll_window
, True, True, 0)
228 self
.add_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
229 self
.connect('button-press-event', self
.button_press
)
230 view
.add_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
231 view
.connect('button-press-event', self
.button_press
)
233 Menu
.set_save_name(APP_NAME
)
234 self
.menu
= Menu
.Menu('main', [
235 Menu
.Action(_('Rip & Encode'), 'rip_n_encode', '', gtk
.STOCK_EXECUTE
),
236 Menu
.Action(_('Reload CD'), 'do_get_tracks', '', gtk
.STOCK_REFRESH
),
237 Menu
.Action(_('Stop'), 'stop', '', gtk
.STOCK_STOP
),
239 Menu
.Action(_('Settings'), 'show_options', '', gtk
.STOCK_PREFERENCES
),
240 Menu
.Action(_("Quit"), 'close', '', gtk
.STOCK_CLOSE
),
242 self
.menu
.attach(self
,self
)
246 self
.ripper_thd
= None
247 self
.encoder_thd
= None
248 self
.is_ripping
= False
249 self
.is_encoding
= False
250 self
.is_cddbing
= False
251 self
.stop_request
= False
253 cd_logic
.set_dev(RIPPER_DEV
.value
)
254 self
.cd_status
= cd_logic
.check_dev()
256 self
.cd_status_changed
= False
258 if self
.cd_status
in [CDROM
.CDS_TRAY_OPEN
, CDROM
.CDS_NO_DISC
]:
262 gtk
.timeout_add(1000, self
.update_gui
)
265 def update_gui(self
):
266 '''Update button status based on current state'''
267 cd_status
= cd_logic
.check_dev()
268 if self
.cd_status
!= cd_status
:
269 self
.cd_status
= cd_status
270 self
.cd_status_changed
= True
272 if self
.is_ripping
or self
.is_encoding
:
273 self
.stop_btn
.set_sensitive(True)
274 self
.rip_btn
.set_sensitive(False)
275 self
.refresh_btn
.set_sensitive(False)
277 if not self
.is_ripping
and not self
.is_encoding
and not self
.is_cddbing
:
278 self
.stop_btn
.set_sensitive(False)
279 self
.rip_btn
.set_sensitive(True)
280 self
.refresh_btn
.set_sensitive(True)
282 #get tracks if cd changed and not doing other things
283 if self
.cd_status_changed
:
284 if self
.cd_status
in [CDROM
.CDS_TRAY_OPEN
, CDROM
.CDS_NO_DISC
]:
288 disc_id
= cd_logic
.get_disc_id()
289 if self
.disc_id
<> disc_id
:
290 self
.disc_id
= disc_id
292 self
.cd_status_changed
= False
294 #need this to keep the timer running(?)
298 def stuff_changed(self
, button
=None):
299 '''Get new text from edit boxes and save it'''
300 self
.genre
= self
.genre_combo
.entry
.get_text()
301 self
.artist
= self
.artist_entry
.get_text()
302 self
.album
= self
.album_entry
.get_text()
303 self
.year
= self
.year_entry
.get_text()
307 '''Run a function in a thread'''
308 thd_it
= Thread(name
='mythread', target
=it
)
309 thd_it
.setDaemon(True)
315 '''Stop current rip/encode process'''
316 self
.stop_request
= True
318 def show_dir(self
, *dummy
):
319 ''' Pops up a filer window. '''
320 temp
= os
.path
.join(os
.path
.expanduser(LIBRARY
.value
), self
.artist
, self
.album
)
321 filer
.show_file(temp
)
323 def do_get_tracks(self
, button
=None):
324 '''Get the track info (cddb and cd) in a thread'''
327 self
.cddb_thd
= self
.runit(self
.get_tracks
)
331 '''Clear all info and display <no disc>'''
332 #print "no disc in tray?"
335 self
.artist_entry
.set_text(_('<no disc>'))
336 self
.album_entry
.set_text('')
337 self
.genre_combo
.entry
.set_text('')
338 self
.year_entry
.set_text('')
339 self
.view
.columns_autosize()
343 def get_tracks(self
):
344 '''Get the track info (cddb and cd)'''
345 self
.is_cddbing
= True
346 stuff
= self
.get_cddb()
348 (count
, artist
, album
, genre
, year
, tracklist
) = stuff
349 #print count, artist, album, genre, year, tracklist
356 self
.tracklist
= tracklist
358 if artist
: self
.artist_entry
.set_text(artist
)
359 if album
: self
.album_entry
.set_text(album
)
360 if genre
: self
.genre_combo
.entry
.set_text(genre
)
361 if year
: self
.year_entry
.set_text(year
)
364 for track
in tracklist
:
366 iter = self
.store
.append(None)
367 self
.store
.set(iter, COL_TRACK
, track
[0])
368 self
.store
.set(iter, COL_TIME
, track
[1])
369 self
.store
.set(iter, COL_ENABLE
, True)
371 self
.view
.columns_autosize()
373 self
.is_cddbing
= False
377 '''Query cddb for track and cd info'''
379 dlg
= gtk
.MessageDialog(buttons
=gtk
.BUTTONS_CANCEL
, message_format
="Getting Track Info.")
380 dlg
.set_position(gtk
.WIN_POS_NONE
)
381 (a
, b
) = dlg
.get_size()
382 (x
, y
) = self
.get_position()
383 (dx
, dy
) = self
.get_size()
384 dlg
.move(x
+dx
/2-a
/2, y
+dy
/2-b
/2)
388 count
= artist
= genre
= album
= year
= ''
392 #Note: all the nested try statements are to ensure that as much
393 #info is processed as possible. One exception should not stop
394 #the whole thing and return nothing.
397 count
= cd_logic
.total_tracks()
398 cddb_id
= cd_logic
.get_cddb_id()
400 #PyCDDB wants a string delimited by spaces, go figure.
403 cddb_id_string
+= str(n
)+' '
405 #print cddb_id, cddb_id_string
407 for i
in range(count
):
408 tracktime
= cd_logic
.get_track_time_total(i
+1)
409 track_time
= time
.strftime('%M:%S', time
.gmtime(tracktime
))
410 tracklist
.append((_('Track')+`i`
,track_time
))
413 db
= PyCDDB
.PyCDDB(CDDB_SERVER
.value
)
414 query_info
= db
.query(cddb_id_string
)
418 dlg
.set_title(_('Got Disc Info'))
421 #make sure we didn't get an error, then query CDDB
422 if len(query_info
) > 0:
424 index
= rndm
.randrange(0, len(query_info
))
425 read_info
= db
.read(query_info
[index
])
428 dlg
.set_title(_('Got Track Info'))
432 (artist
, album
) = query_info
[index
]['title'].split('/')
433 artist
= artist
.strip()
434 album
= album
.strip()
435 genre
= query_info
[index
]['category']
436 if genre
in ['misc', 'data']:
439 print query_info
['year']
440 print read_info
['EXTD']
441 print read_info
['YEARD']
443 #x = re.match(r'.*YEAR: (.+).*',read_info['EXTD'])
450 if len(read_info
['TTITLE']) > 0:
451 for i
in range(count
):
453 track_name
= read_info
['TTITLE'][i
]
454 track_time
= tracklist
[i
][1]
455 #print i, track_name, track_time
456 tracklist
[i
] = (track_name
, track_time
)
468 return count
, artist
, album
, genre
, year
, tracklist
471 def get_cdda2wav(self
, tracknum
, track
):
472 '''Run cdda2wav to rip a track from the CD'''
473 cdda2wav_cmd
= RIPPER
.value
474 cdda2wav_dev
= RIPPER_DEV
.value
475 cdda2wav_lun
= RIPPER_LUN
.value
476 cdda2wav_args
= '-g -D%s -A%s -t %d "%s"' % (
477 cdda2wav_lun
, cdda2wav_dev
, tracknum
+1, strip_illegal(track
))
478 cdda2wav_opts
= RIPPER_OPTS
.value
479 #print cdda2wav_opts, cdda2wav_args
481 thing
= popen2
.Popen4(cdda2wav_cmd
+' '+cdda2wav_opts
+' '+cdda2wav_args
)
482 outfile
= thing
.fromchild
485 line
= myreadline(outfile
)
487 x
= re
.match("(.+[\s]+)([0-9]+)%", line
)
489 percent
= int(x
.group(2))
490 self
.status_update(tracknum
, 'rip', percent
)
493 if self
.stop_request
:
496 if self
.stop_request
:
497 os
.kill(thing
.pid
, signal
.SIGKILL
)
500 self
.status_update(tracknum
, 'rip', 100)
505 def get_lame(self
, tracknum
, track
, artist
, genre
, album
, year
):
506 '''Run lame to encode a wav file to mp3'''
512 lame_cmd
= MP3_ENCODER
.value
513 lame_opts
= MP3_ENCODER_OPTS
.value
514 lame_tags
= '--ta "%s" --tt "%s" --tl "%s" --tg "%s" --tn %d --ty %d' % (
515 artist
, track
, album
, genre
, tracknum
+1, int_year
)
516 lame_args
= '"%s" "%s"' % (strip_illegal(track
)+'.wav', strip_illegal(track
)+'.mp3')
518 #print lame_opts, lame_tags, lame_args
520 thing
= popen2
.Popen4(lame_cmd
+' '+lame_opts
+' '+lame_tags
+' '+lame_args
)
521 outfile
= thing
.fromchild
524 line
= myreadline(outfile
)
527 #for some reason getting this right for lame was a royal pain.
528 x
= re
.match(r
"^[\s]+([0-9]+)/([0-9]+)", line
)
530 percent
= int(100 * (float(x
.group(1)) / float(x
.group(2))))
531 self
.status_update(tracknum
, 'enc', percent
)
534 if self
.stop_request
:
537 if self
.stop_request
:
538 os
.kill(thing
.pid
, signal
.SIGKILL
)
541 filename
= strip_illegal(track
)+'.mp3'
542 xattr
.setxattr(filename
, 'user.Title', track
)
543 xattr
.setxattr(filename
, 'user.Artist', artist
)
544 xattr
.setxattr(filename
, 'user.Album', album
)
545 xattr
.setxattr(filename
, 'user.Genre', genre
)
546 xattr
.setxattr(filename
, 'user.Track', '%d' % tracknum
)
547 xattr
.setxattr(filename
, 'user.Year', year
)
552 self
.status_update(tracknum
, 'enc', 100)
557 def get_ogg(self
, tracknum
, track
, artist
, genre
, album
, year
):
558 '''Run oggenc to encode a wav file to ogg'''
564 ogg_cmd
= OGG_ENCODER
.value
565 ogg_opts
= OGG_ENCODER_OPTS
.value
566 ogg_tags
= '-a "%s" -t "%s" -l "%s" -G "%s" -N %d -d %d' % (
567 artist
, track
, album
, genre
, tracknum
+1, int_year
)
568 ogg_args
= '"%s"' % (strip_illegal(track
)+'.wav')
570 #print ogg_opts, ogg_tags, ogg_args
572 thing
= popen2
.Popen4(ogg_cmd
+' '+ogg_opts
+' '+ogg_tags
+' '+ogg_args
)
573 outfile
= thing
.fromchild
576 line
= myreadline(outfile
)
579 #for some reason getting this right for ogg was a royal pain.
580 x
= re
.match('^.*\[[\s]*([.0-9]+)%\]', line
)
582 percent
= float(x
.group(1))
583 self
.status_update(tracknum
, 'enc', percent
)
586 if self
.stop_request
:
589 if self
.stop_request
:
590 os
.kill(thing
.pid
, signal
.SIGKILL
)
593 filename
= strip_illegal(track
)+'.ogg'
594 xattr
.setxattr(filename
, 'user.Title', track
)
595 xattr
.setxattr(filename
, 'user.Artist', artist
)
596 xattr
.setxattr(filename
, 'user.Album', album
)
597 xattr
.setxattr(filename
, 'user.Genre', genre
)
598 xattr
.setxattr(filename
, 'user.Track', '%d' % tracknum
)
599 xattr
.setxattr(filename
, 'user.Year', year
)
604 self
.status_update(tracknum
, 'enc', 100)
609 def rip_n_encode(self
, button
=None):
610 '''Process all selected tracks (rip and encode)'''
612 os
.chdir(os
.path
.expanduser(LIBRARY
.value
))
615 os
.mkdir(os
.path
.expanduser(LIBRARY
.value
))
616 os
.chdir(os
.path
.expanduser(LIBRARY
.value
))
618 rox
.alert("Failed to find or create Library dir")
620 if self
.count
and self
.artist
and self
.album
:
621 try: os
.mkdir(self
.artist
)
624 try: os
.mkdir(self
.artist
+'/'+self
.album
)
627 try: os
.chdir(self
.artist
+'/'+self
.album
)
630 self
.stop_request
= False
632 #the queue to feed tracks from ripper to encoder
633 self
.wavqueue
= Queue
.Queue(1000)
635 self
.ripper_thd
= self
.runit(self
.ripit
)
636 self
.encoder_thd
= self
.runit(self
.encodeit
)
640 '''Thread to rip all selected tracks'''
641 self
.is_ripping
= True
642 for i
in range(self
.count
):
643 if self
.stop_request
:
646 if self
.store
[i
][COL_ENABLE
]:
647 track
= self
.store
[i
][COL_TRACK
]
649 status
= self
.get_cdda2wav(i
, track
)
651 print 'cdda2wav died %d' % status
652 self
.status_update(i
, 'rip_error', 0)
654 #push this track on the queue for the encoder
656 self
.wavqueue
.put((track
, i
))
658 #push None object to tell encoder we're done
660 self
.wavqueue
.put((None, None))
662 self
.is_ripping
= False
664 if EJECT_AFTER_RIP
.int_value
:
669 '''Thread to encode all tracks from the wavqueue'''
670 self
.is_encoding
= True
672 if self
.stop_request
:
674 (track
, tracknum
) = self
.wavqueue
.get(True)
678 if ENCODER
.value
== 'MP3':
679 status
= self
.get_lame(tracknum
, track
, self
.artist
, self
.genre
, self
.album
, self
.year
)
681 status
= self
.get_ogg(tracknum
, track
, self
.artist
, self
.genre
, self
.album
, self
.year
)
684 print 'encoder died %d' % status
685 self
.status_update(tracknum
, 'enc_error', 0)
686 try: os
.unlink(strip_illegal(track
)+".wav")
688 try: os
.unlink(strip_illegal(track
)+".inf")
691 self
.is_encoding
= False
695 def status_update(self
, row
, state
, percent
):
696 '''Callback from rip/encode threads to update display'''
699 iter = self
.store
.get_iter((row
,))
704 self
.store
.set_value(iter, COL_STATUS
, _('Ripping')+': %d%%' % percent
)
706 self
.store
.set_value(iter, COL_STATUS
, _('Ripping')+': '+_('done'))
710 self
.store
.set_value(iter, COL_STATUS
, _('Encoding')+': %d%%' % percent
)
712 self
.store
.set_value(iter, COL_STATUS
, _('Encoding')+': '+_('done'))
714 if state
== 'rip_error':
715 self
.store
.set_value(iter, COL_STATUS
, _('Ripping')+': '+_('error'))
717 if state
== 'enc_error':
718 self
.store
.set_value(iter, COL_STATUS
, _('Encoding')+': '+_('error'))
723 def activate(self
, view
, path
, column
):
724 '''Edit a track name'''
725 model
, iter = self
.view
.get_selection().get_selected()
727 track
= model
.get_value(iter, COL_TRACK
)
728 dlg
= gtk
.Dialog(APP_NAME
)
729 dlg
.set_position(gtk
.WIN_POS_NONE
)
730 dlg
.set_default_size(350, 100)
731 (a
, b
) = dlg
.get_size()
732 (x
, y
) = self
.get_position()
733 (dx
, dy
) = self
.get_size()
734 dlg
.move(x
+dx
/2-a
/2, y
+dy
/2-b
/2)
738 entry
.set_text(track
)
739 dlg
.set_position(gtk
.WIN_POS_MOUSE
)
741 entry
.set_activates_default(True)
742 dlg
.vbox
.pack_start(entry
)
744 dlg
.add_button(gtk
.STOCK_OK
, gtk
.RESPONSE_OK
)
745 dlg
.add_button(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
746 dlg
.set_default_response(gtk
.RESPONSE_OK
)
749 if response
== gtk
.RESPONSE_OK
:
750 track
= entry
.get_text()
752 model
.set_value(iter, COL_TRACK
, track
)
753 self
.view
.columns_autosize()
758 def toggle_check(self
, cell
, rownum
):
759 '''Toggle state for each song'''
760 row
= self
.store
[rownum
]
761 row
[COL_ENABLE
] = not row
[COL_ENABLE
]
762 self
.store
.row_changed(rownum
, row
.iter)
765 def set_selection(self
, thing
):
766 '''Get current selection'''
767 #model, iter = self.view.get_selection().get_selected()
769 # track = model.get_value(iter, COL_TRACK)
771 def button_press(self
, text
, event
):
772 '''Popup menu handler'''
773 if event
.button
!= 3:
775 self
.menu
.popup(self
, event
)
778 def show_options(self
, button
=None):
779 '''Show Options dialog'''
782 def get_options(self
):
783 '''Get changed Options'''
786 def delete_event(self
, ev
, e1
):
790 def close(self
, button
= None):
791 '''We're outta here!'''