Last minute 'Oops!' - application didn't exit because of TimeoutBlocker toplevel_ref()
[rox-ripper.git] / ripper.py
blob54b910463e145838a229136828aff1e39dae45b2
1 """
2 ripper.py
3 GUI front-end to cdda2wav and lame.
5 Copyright 2004-2006 Kenneth Hayber <ken@hayber.us>
6 All rights reserved.
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
21 """
23 import gtk, os, sys, signal, re, string, socket, time, popen2, Queue
24 from random import Random
26 import rox
27 from rox import i18n, app_options, Menu, filer, InfoWin, tasks
28 from rox.options import Option
30 import PyCDDB, cd_logic, CDROM, genres
32 try:
33 import xattr
34 HAVE_XATTR = True
35 except:
36 HAVE_XATTR = False
38 _ = rox.i18n.translation(os.path.join(rox.app_dir, 'Messages'))
40 #Who am I and how did I get here?
41 APP_NAME = 'Ripper' #I could call it Mr. Giles, but that would be gay.
42 APP_PATH = rox.app_dir
45 #Options.xml processing
46 from rox import choices
47 choices.migrate(APP_NAME, 'hayber.us')
48 rox.setup_app_options(APP_NAME, site='hayber.us')
49 Menu.set_save_name(APP_NAME, site='hayber.us')
51 #assume that everyone puts their music in ~/Music
52 LIBRARY = Option('library', '~/Music')
54 #RIPPER options
55 RIPPER = Option('ripper', 'cdda2wav')
56 RIPPER_DEV = Option('ripper_dev', '/dev/cdrom')
57 RIPPER_LUN = Option('ripper_lun', 'ATAPI:0,1,0')
58 RIPPER_OPTS = Option('ripper_opts', '-x -H')
60 EJECT_AFTER_RIP = Option('eject_after_rip', '0')
62 #ENCODER options
63 ENCODER = Option('encoder', 'MP3')
65 MP3_ENCODER = Option('mp3_encoder', 'lame')
66 MP3_ENCODER_OPTS = Option('mp3_encoder_opts', '--vbr-new -b160 --nohist --add-id3v2')
68 OGG_ENCODER = Option('ogg_encoder', 'oggenc')
69 OGG_ENCODER_OPTS = Option('ogg_encoder_opts', '-q5')
71 #CDDB Server and Options
72 CDDB_SERVER = Option('cddb_server', 'http://freedb.freedb.org/~cddb/cddb.cgi')
74 rox.app_options.notify()
77 #Column indicies
78 COL_ENABLE = 0
79 COL_TRACK = 1
80 COL_TIME = 2
81 COL_STATUS = 3
83 DEBUG = 0
84 def dbg(*args):
85 if DEBUG:
86 import sys
87 print >>sys.stderr, args
90 # My gentoo python doesn't have universal line ending support compiled in
91 # and these guys (cdda2wav and lame) use CR line endings to pretty-up their output.
92 def myreadline(file):
93 '''Return a line of input using \r or \n as terminators'''
94 line = ''
95 while '\n' not in line and '\r' not in line:
96 char = file.read(1)
97 if char == '': return line
98 line += char
99 return line
102 def which(filename):
103 '''Return the full path of an executable if found on the path'''
104 if (filename == None) or (filename == ''):
105 return None
107 env_path = os.getenv('PATH').split(':')
108 for p in env_path:
109 if os.access(p+'/'+filename, os.X_OK):
110 return p+'/'+filename
111 return None
114 def strip_illegal(instr):
115 '''remove illegal (filename) characters from string'''
116 str = instr
117 str = str.strip()
118 str = string.translate(str, string.maketrans(r'/+{}*.?', r'--()___'))
119 return str
122 class Ripper(rox.Window):
123 '''Rip and Encode a CD'''
124 def __init__(self):
125 rox.Window.__init__(self)
127 self.set_title(APP_NAME)
128 self.set_default_size(450, 500)
129 self.set_position(gtk.WIN_POS_MOUSE)
131 #capture wm delete event
132 self.connect("delete_event", self.delete_event)
134 # Update things when options change
135 rox.app_options.add_notify(self.get_options)
137 self.build_gui()
138 self.build_toolbar()
139 self.build_menu()
141 # Create layout, pack and show widgets
142 self.vbox = gtk.VBox()
143 self.add(self.vbox)
144 self.vbox.pack_start(self.toolbar, False, True, 0)
145 self.vbox.pack_start(self.table, False, True, 0)
146 self.vbox.pack_start(self.scroll_window, True, True, 0)
147 self.vbox.show_all()
149 # Defaults and Misc
150 self.is_ripping = False
151 self.is_encoding = False
152 self.is_cddbing = False
153 self.stop_request = False
154 self.closing = False
156 cd_logic.set_dev(RIPPER_DEV.value)
157 self.cd_status = cd_logic.check_dev()
158 self.disc_id = None
159 self.cd_status_changed = False
161 if self.cd_status in [CDROM.CDS_TRAY_OPEN, CDROM.CDS_NO_DISC]:
162 self.no_disc()
163 else:
164 self.do_get_tracks()
166 tasks.Task(self.update_gui())
169 def build_menu(self):
170 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
171 self.connect('button-press-event', self.button_press)
172 self.view.add_events(gtk.gdk.BUTTON_PRESS_MASK)
173 self.view.connect('button-press-event', self.button_press)
175 self.menu = Menu.Menu('main', [
176 Menu.Action(_('Rip & Encode'), 'rip_n_encode', '', gtk.STOCK_EXECUTE),
177 Menu.Action(_('Reload CD'), 'do_get_tracks', '', gtk.STOCK_REFRESH),
178 Menu.Action(_('Stop'), 'stop', '', gtk.STOCK_STOP),
179 Menu.Separator(),
180 Menu.Action(_('Options'), 'show_options', '', gtk.STOCK_PREFERENCES),
181 Menu.Action(_('Info'), 'get_info', '', gtk.STOCK_DIALOG_INFO),
182 Menu.Action(_("Quit"), 'close', '', gtk.STOCK_CLOSE),
184 self.menu.attach(self,self)
187 def build_toolbar(self):
188 self.toolbar = gtk.Toolbar()
189 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
190 self.toolbar.insert_stock(gtk.STOCK_PREFERENCES, _('Settings'), None, self.show_options, None, 0)
191 self.stop_btn = self.toolbar.insert_stock(gtk.STOCK_STOP, _('Stop'), None, self.stop, None, 0)
192 self.rip_btn = self.toolbar.insert_stock(gtk.STOCK_EXECUTE, _('Rip & Encode'), None, self.rip_n_encode, None, 0)
193 self.refresh_btn = self.toolbar.insert_stock(gtk.STOCK_REFRESH, _('Reload CD'), None, self.do_get_tracks, None, 0)
194 self.toolbar.insert_stock(gtk.STOCK_GO_UP, _('Show destination dir'), None, self.show_dir, None, 0)
195 self.toolbar.insert_stock(gtk.STOCK_CLOSE, _('Close'), None, self.close, None, 0)
198 def build_gui(self):
199 swin = gtk.ScrolledWindow()
200 self.scroll_window = swin
201 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
202 swin.set_shadow_type(gtk.SHADOW_IN)
204 self.store = gtk.ListStore(int, str, str, str)
205 view = gtk.TreeView(self.store)
206 self.view = view
207 swin.add(view)
208 view.set_rules_hint(True)
210 cell = gtk.CellRendererToggle()
211 cell.connect('toggled', self.toggle_check)
212 column = gtk.TreeViewColumn('', cell, active=COL_ENABLE)
213 view.append_column(column)
214 column.set_resizable(False)
215 column.set_reorderable(False)
217 cell = gtk.CellRendererText()
218 column = gtk.TreeViewColumn(_('Track'), cell, text = COL_TRACK)
219 view.append_column(column)
220 column.set_resizable(True)
221 column.set_reorderable(False)
223 cell = gtk.CellRendererText()
224 column = gtk.TreeViewColumn(_('Time'), cell, text = COL_TIME)
225 view.append_column(column)
226 column.set_resizable(True)
227 column.set_reorderable(False)
229 cell = gtk.CellRendererText()
230 column = gtk.TreeViewColumn(_('Status'), cell, text = COL_STATUS)
231 view.append_column(column)
232 column.set_resizable(True)
233 column.set_reorderable(False)
235 view.connect('row-activated', self.activate)
236 self.selection = view.get_selection()
237 self.handler = self.selection.connect('changed', self.set_selection)
239 self.table = gtk.Table(5, 2, False)
240 x_pad = 2
241 y_pad = 1
243 self.artist_entry = gtk.Entry(max=255)
244 self.artist_entry.connect('changed', self.stuff_changed)
245 self.table.attach(gtk.Label(str=_('Artist')), 0, 1, 2, 3, 0, 0, 4, y_pad)
246 self.table.attach(self.artist_entry, 1, 2, 2, 3, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
248 self.album_entry = gtk.Entry(max=255)
249 self.album_entry.connect('changed', self.stuff_changed)
250 self.table.attach(gtk.Label(str=_('Album')), 0, 1, 3, 4, 0, 0, 4, y_pad)
251 self.table.attach(self.album_entry, 1, 2, 3, 4, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
253 genres.genre_list.sort()
254 self.genre_combo = gtk.Combo()
255 self.genre_combo.set_popdown_strings(genres.genre_list)
256 self.genre_combo.entry.connect('changed', self.stuff_changed)
257 self.table.attach(gtk.Label(str=_('Genre')), 0, 1, 4, 5, 0, 0, 4, y_pad)
258 self.table.attach(self.genre_combo, 1, 2, 4, 5, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
260 self.year_entry = gtk.Entry(max=4)
261 self.year_entry.connect('changed', self.stuff_changed)
262 self.table.attach(gtk.Label(str=_('Year')), 0, 1, 5, 6, 0, 0, 4, y_pad)
263 self.table.attach(self.year_entry, 1, 2, 5, 6, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
266 def update_gui(self):
267 '''Update UI based on current state'''
268 while True:
269 cd_status = cd_logic.check_dev()
270 if self.cd_status != cd_status:
271 self.cd_status = cd_status
272 self.cd_status_changed = True
274 if self.is_ripping or self.is_encoding:
275 self.stop_btn.set_sensitive(True)
276 self.rip_btn.set_sensitive(False)
277 self.refresh_btn.set_sensitive(False)
279 if not self.is_ripping and not self.is_encoding and not self.is_cddbing:
280 self.stop_btn.set_sensitive(False)
281 self.rip_btn.set_sensitive(True)
282 self.refresh_btn.set_sensitive(True)
284 #get tracks if cd changed and not doing other things
285 if self.cd_status_changed:
286 if self.cd_status in [CDROM.CDS_TRAY_OPEN, CDROM.CDS_NO_DISC]:
287 self.no_disc()
288 self.disc_id = None
289 else:
290 disc_id = cd_logic.get_disc_id()
291 if self.disc_id <> disc_id:
292 self.disc_id = disc_id
293 self.do_get_tracks()
294 self.cd_status_changed = False
296 if self.closing:
297 break
299 yield tasks.TimeoutBlocker(1)
302 def stuff_changed(self, button=None):
303 '''Get new text from edit boxes and save it'''
304 self.genre = self.genre_combo.entry.get_text()
305 self.artist = self.artist_entry.get_text()
306 self.album = self.album_entry.get_text()
307 self.year = self.year_entry.get_text()
310 def stop(self, *it):
311 '''Stop current rip/encode process'''
312 self.stop_request = True
315 def show_dir(self, *dummy):
316 ''' Pops up a filer window. '''
317 temp = os.path.join(os.path.expanduser(LIBRARY.value), self.artist, self.album)
318 filer.show_file(temp)
321 def do_get_tracks(self, button=None):
322 '''Get the track info (cddb and cd) in a thread'''
323 if self.is_ripping:
324 return
325 tasks.Task(self.get_tracks())
328 def no_disc(self):
329 '''Clear all info and display <no disc>'''
330 dbg("no disc in tray?")
331 self.store.clear()
332 self.artist_entry.set_text(_('<no disc>'))
333 self.album_entry.set_text('')
334 self.genre_combo.entry.set_text('')
335 self.year_entry.set_text('')
336 self.view.columns_autosize()
339 def get_tracks(self):
340 '''Get the track info (cddb and cd)'''
341 self.is_cddbing = True
342 stuff = self.get_cddb()
343 (count, artist, album, genre, year, tracklist) = stuff
345 yield None
347 self.artist = artist
348 self.count = count
349 self.album = album
350 self.genre = genre
351 self.year = year
352 self.tracklist = tracklist
354 if artist: self.artist_entry.set_text(artist)
355 if album: self.album_entry.set_text(album)
356 if genre: self.genre_combo.entry.set_text(genre)
357 if year: self.year_entry.set_text(year)
359 self.store.clear()
360 for track in tracklist:
361 dbg(track)
362 iter = self.store.append(None)
363 self.store.set(iter, COL_TRACK, track[0])
364 self.store.set(iter, COL_TIME, track[1])
365 self.store.set(iter, COL_ENABLE, True)
366 yield None
368 self.view.columns_autosize()
369 self.is_cddbing = False
372 def get_cddb(self):
373 '''Query cddb for track and cd info'''
374 dlg = gtk.MessageDialog(buttons=gtk.BUTTONS_CANCEL, message_format="Getting Track Info.")
375 dlg.show()
376 while gtk.events_pending():
377 gtk.main_iteration()
379 count = artist = genre = album = year = ''
380 tracklist = []
381 tracktime = []
383 #Note: all the nested try|except|pass statements are to ensure that as much
384 #info is processed as possible. One exception should not stop
385 #the whole thing and return nothing.
387 try:
388 count = cd_logic.total_tracks()
389 cddb_id = cd_logic.get_cddb_id()
391 #PyCDDB wants a string delimited by spaces, go figure.
392 cddb_id_string = ''
393 for n in cddb_id:
394 cddb_id_string += str(n)+' '
395 #dbg(disc_id, cddb_id, cddb_id_string)
397 for i in range(count):
398 tracktime = cd_logic.get_track_time_total(i+1)
399 track_time = time.strftime('%M:%S', time.gmtime(tracktime))
400 tracklist.append((_('Track')+`i`,track_time))
402 try:
403 db = PyCDDB.PyCDDB(CDDB_SERVER.value)
404 query_info = db.query(cddb_id_string)
405 #dbg(query_info)
407 dlg.set_title(_('Got Disc Info'))
408 while gtk.events_pending():
409 gtk.main_iteration()
411 #make sure we didn't get an error, then query CDDB
412 if len(query_info) > 0:
413 rndm = Random()
414 index = rndm.randrange(0, len(query_info))
415 read_info = db.read(query_info[index])
416 #dbg(read_info)
417 dlg.set_title(_('Got Track Info'))
418 while gtk.events_pending():
419 gtk.main_iteration()
421 try:
422 (artist, album) = query_info[index]['title'].split('/')
423 artist = artist.strip().decode('latin-1', 'replace')
424 album = album.strip().decode('latin-1', 'replace')
425 genre = query_info[index]['category']
426 if genre in ['misc', 'data']:
427 genre = 'Other'
429 #dbg(query_info['year'])
430 #dbg(read_info['EXTD'])
431 #dbg(read_info['YEARD'])
433 #x = re.match(r'.*YEAR: (.+).*',read_info['EXTD'])
434 #if x:
435 # print x.group(1)
436 # year = x.group(1)
437 except:
438 pass
440 if len(read_info['TTITLE']) > 0:
441 for i in range(count):
442 try:
443 track_name = read_info['TTITLE'][i].decode('latin-1', 'replace')
444 track_time = tracklist[i][1]
445 #dbg(i, track_name, track_time)
446 tracklist[i] = (track_name, track_time)
447 except:
448 pass
449 except:
450 pass
452 except:
453 pass
455 dlg.destroy()
456 return count, artist, album, genre, year, tracklist
459 def run_cdda2wav(self, tracknum, track):
460 '''Run cdda2wav to rip a track from the CD'''
461 cdda2wav_cmd = RIPPER.value
462 cdda2wav_dev = RIPPER_DEV.value
463 cdda2wav_lun = RIPPER_LUN.value
464 cdda2wav_args = '-g -D%s -A%s -t %d "%s"' % (
465 cdda2wav_lun, cdda2wav_dev, tracknum+1, strip_illegal(track))
466 cdda2wav_opts = RIPPER_OPTS.value
468 thing = popen2.Popen4(cdda2wav_cmd+' '+cdda2wav_opts+' '+cdda2wav_args )
469 return thing
472 def run_lame(self, tracknum, track, artist, genre, album, year):
473 '''Run lame to encode a wav file to mp3'''
474 try:
475 int_year = int(year)
476 except:
477 int_year = 1
479 lame_cmd = MP3_ENCODER.value
480 lame_opts = MP3_ENCODER_OPTS.value
481 lame_tags = '--ta "%s" --tt "%s" --tl "%s" --tg "%s" --tn %d --ty %d' % (
482 artist, track, album, genre, tracknum+1, int_year)
483 lame_args = '"%s" "%s"' % (strip_illegal(track)+'.wav', strip_illegal(track)+'.mp3')
485 #dbg(lame_opts, lame_tags, lame_args)
486 thing = popen2.Popen4(lame_cmd+' '+lame_opts+' '+lame_tags+' '+lame_args )
487 return thing
490 def run_ogg(self, tracknum, track, artist, genre, album, year):
491 '''Run oggenc to encode a wav file to ogg'''
492 try:
493 int_year = int(year)
494 except:
495 int_year = 1
497 ogg_cmd = OGG_ENCODER.value
498 ogg_opts = OGG_ENCODER_OPTS.value
499 ogg_tags = '-a "%s" -t "%s" -l "%s" -G "%s" -N %d -d %d' % (
500 artist, track, album, genre, tracknum+1, int_year)
501 ogg_args = '"%s"' % (strip_illegal(track)+'.wav')
503 #dbg(ogg_opts, ogg_tags, ogg_args)
504 thing = popen2.Popen4(ogg_cmd+' '+ogg_opts+' '+ogg_tags+' '+ogg_args )
505 return thing
508 def rip_n_encode(self, button=None):
509 '''Process all selected tracks (rip and encode)'''
510 try:
511 os.chdir(os.path.expanduser(LIBRARY.value))
512 except:
513 try:
514 os.mkdir(os.path.expanduser(LIBRARY.value))
515 os.chdir(os.path.expanduser(LIBRARY.value))
516 except:
517 rox.alert("Failed to find or create Library dir")
519 if self.count and self.artist and self.album:
520 try: os.mkdir(self.artist)
521 except: pass
523 try: os.mkdir(self.artist+'/'+self.album)
524 except: pass
526 try: os.chdir(self.artist+'/'+self.album)
527 except: pass
529 self.stop_request = False
531 #the queue to feed tracks from ripper to encoder
532 self.wavqueue = Queue.Queue(1000)
534 tasks.Task(self.ripit())
535 tasks.Task(self.encodeit())
538 def ripit(self):
539 '''Thread to rip all selected tracks'''
540 self.is_ripping = True
541 for tracknum in range(self.count):
542 if self.stop_request:
543 break;
545 if self.store[tracknum][COL_ENABLE]:
546 track = self.store[tracknum][COL_TRACK]
547 thing = self.run_cdda2wav(tracknum, track)
548 outfile = thing.fromchild
549 while True:
550 blocker = tasks.InputBlocker(outfile)
551 yield blocker
553 if self.stop_request:
554 os.kill(thing.pid, signal.SIGKILL)
555 break
557 line = myreadline(outfile)
558 if line:
559 x = re.match(".*([ 0-9][0-9])%", line)
560 if x:
561 percent = int(x.group(1))
562 self.status_update(tracknum, 'rip', percent)
563 else:
564 break
566 status = thing.wait()
567 self.status_update(tracknum, 'rip', 100)
569 if status <> 0:
570 #dbg('cdda2wav died %d' % status)
571 self.status_update(tracknum, 'rip_error', 0)
572 else:
573 #push this track on the queue for the encoder
574 if self.wavqueue:
575 self.wavqueue.put((track, tracknum))
577 #push None object to tell encoder we're done
578 if self.wavqueue:
579 self.wavqueue.put((None, None))
581 self.is_ripping = False
582 cd_logic.stop()
583 if EJECT_AFTER_RIP.int_value:
584 cd_logic.eject()
587 def encodeit(self):
588 '''Thread to encode all tracks from the wavqueue'''
589 self.is_encoding = True
590 while True:
591 if self.stop_request:
592 break
594 try:
595 (track, tracknum) = self.wavqueue.get(False)
596 except Queue.Empty:
597 yield tasks.TimeoutBlocker(1)
598 continue
600 if track == None:
601 break
603 if ENCODER.value == 'MP3':
604 thing = self.run_lame(tracknum, track, self.artist, self.genre, self.album, self.year)
605 else:
606 thing = self.run_ogg(tracknum, track, self.artist, self.genre, self.album, self.year)
608 outfile = thing.fromchild
609 while True:
610 blocker = tasks.InputBlocker(outfile)
611 yield blocker
613 if self.stop_request:
614 os.kill(thing.pid, signal.SIGKILL)
615 break
617 line = myreadline(outfile)
618 #dbg(line)
619 if line:
620 if ENCODER.value == 'MP3':
621 x = re.match(r"^[\s]+([0-9]+)/([0-9]+)", line)
622 if x:
623 percent = int(100 * (float(x.group(1)) / float(x.group(2))))
624 self.status_update(tracknum, 'enc', percent)
625 else: #OGG
626 x = re.match('^.*\[[\s]*([.0-9]+)%\]', line)
627 if x:
628 percent = float(x.group(1))
629 self.status_update(tracknum, 'enc', percent)
630 else:
631 break
633 status = thing.wait()
634 self.status_update(tracknum, 'enc', 100)
636 if status <> 0:
637 #dbg('encoder died %d' % status)
638 self.status_update(tracknum, 'enc_error', 0)
640 try: os.unlink(strip_illegal(track)+".wav")
641 except: pass
642 try: os.unlink(strip_illegal(track)+".inf")
643 except: pass
645 self.is_encoding = False
648 def status_update(self, row, state, percent):
649 '''Callback from rip/encode tasks to update display'''
650 iter = self.store.get_iter((row,))
651 if not iter: return
652 if state == 'rip':
653 if percent < 100:
654 self.store.set_value(iter, COL_STATUS, _('Ripping')+': %d%%' % percent)
655 else:
656 self.store.set_value(iter, COL_STATUS, _('Ripping')+': '+_('done'))
657 if state == 'enc':
658 if percent < 100:
659 self.store.set_value(iter, COL_STATUS, _('Encoding')+': %d%%' % percent)
660 else:
661 self.store.set_value(iter, COL_STATUS, _('Encoding')+': '+_('done'))
662 if state == 'rip_error':
663 self.store.set_value(iter, COL_STATUS, _('Ripping')+': '+_('error'))
664 if state == 'enc_error':
665 self.store.set_value(iter, COL_STATUS, _('Encoding')+': '+_('error'))
668 def activate(self, view, path, column):
669 '''Edit a track name'''
670 model, iter = self.view.get_selection().get_selected()
671 if iter:
672 track = model.get_value(iter, COL_TRACK)
673 dlg = gtk.Dialog(APP_NAME)
674 dlg.set_default_size(350, 100)
675 dlg.show()
677 entry = gtk.Entry()
678 entry.set_text(track)
679 entry.show()
680 entry.set_activates_default(True)
681 dlg.vbox.pack_start(entry)
683 dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
684 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
685 dlg.set_default_response(gtk.RESPONSE_OK)
686 response = dlg.run()
688 if response == gtk.RESPONSE_OK:
689 track = entry.get_text()
690 #dbg(track)
691 model.set_value(iter, COL_TRACK, track)
692 self.view.columns_autosize()
694 dlg.destroy()
697 def toggle_check(self, cell, rownum):
698 '''Toggle state for each song'''
699 row = self.store[rownum]
700 row[COL_ENABLE] = not row[COL_ENABLE]
701 self.store.row_changed(rownum, row.iter)
704 def set_selection(self, thing):
705 '''Get current selection'''
706 #model, iter = self.view.get_selection().get_selected()
707 #if iter:
708 # track = model.get_value(iter, COL_TRACK)
710 def button_press(self, text, event):
711 '''Popup menu handler'''
712 if event.button != 3:
713 return 0
714 self.menu.popup(self, event)
715 return 1
717 def show_options(self, button=None):
718 '''Show Options dialog'''
719 rox.edit_options()
721 def get_options(self):
722 '''Get changed Options'''
723 pass
725 def get_info(self):
726 InfoWin.infowin(APP_NAME)
728 def delete_event(self, ev, e1):
729 '''Bye-bye'''
730 self.close()
732 def close(self, button = None):
733 '''We are outta here!'''
734 self.stop()
735 self.closing = True
736 self.destroy()