#! /usr/bin/env python # # Redeye is a CD burning front end. # Copyright (C) 2003 Michael Urman # # Redeye is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Redeye is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Redeye; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # __version__ = '0.0.19' import errno, os, sys, stat, time try: import pygtk pygtk.require("2.0") except: pass import gtk #import gtk.glade import gobject import pango class program: status = None done = 0 def findprogram(self, name): for path in os.environ['PATH'].split(os.pathsep): bin = os.path.join(path, name) if os.path.exists(bin): #mode = os.stat(bin)[stat.ST_MODE] #if mode & stat.S_IXOTH: return bin raise ValueError("Can't find %s in $PATH" % name) def run_for_stdout(self): #__import__('pprint').pprint(self.command()) stdin, stdout, stderr = os.popen3(self.command()) stdin.close() self.stderr_last = stderr.read() stderr.close() return stdout.read() def command(self): raise NotImplementedError("Program is an abstract class") def execute(self, stdin=None, stdout=None, stderr=None): """Execute using provided pipes, or new ones for each not provided. Set input to use stdin[0], and hold stdin[1]; set outputs to use stdxxx[1] and hold stdxxx[0]""" if stdin == None: stdin = os.pipe() if stdout == None: stdout = os.pipe() if stderr == None: stderr = os.pipe() cmd = self.command() self.progpid = os.fork() if self.progpid == 0: # Child - hook up, and stuff. os.dup2(stdin[0], sys.stdin.fileno()) os.dup2(stdout[1], sys.stdout.fileno()) os.dup2(stderr[1], sys.stderr.fileno()) # close off all other descriptors; includes other half of pipes for i in range(3, 256): try: os.close(i) except OSError: pass os.execvp(cmd[0], cmd) # child has exec'd; only parent from here os.close(stdin[0]) try: self.stdin = os.fdopen(stdin[1], 'w') except OSError: pass os.close(stdout[1]) try: self.stdout = os.fdopen(stdout[0], 'r') except OSError: pass os.close(stderr[1]) try: self.stderr = os.fdopen(stderr[0], 'r') except OSError: pass __import__('pprint').pprint(cmd) self.stdout_buf = '' self.stderr_buf = '' def poll(self): if self.status is None: try: pid, status = os.waitpid(self.progpid, os.WNOHANG) if pid == self.progpid: self.status = status except os.error: pass return self.status def wait(self): if self.status is None: pid, status = os.waitpid(self.progpid, 0) if pid == self.progpid: self.status = status return self.status class mount(program): def __init__(self): try: self.mountbin = self.findprogram('mount') self.umountbin = self.findprogram('umount') except ValueError: self.mountbin = self.umountbin = None def mount(self, path): if self.mountbin is not None: return os.system('%s %s' % (self.mountbin, path)) return 0 def unmount(self, path): if self.umountbin is not None: return os.system('%s %s' % (self.umountbin, path)) return 0 class cdrecord(program): device = None write_time = 0.0 fixate_time = 0.0 def __init__(self): self.binary_name = 'cdrecord' try: self.binary = self.findprogram('cdrecord') except ValueError: self.binary = None self.tracks = [] params = [ ('-dummy', 'test_burn', 0, False, "Go through the recording process, but with the laser off"), ('-dao', 'session_at_once', 0, False, "Set Session/Disc At Once mode; requires knowing track size"), ('-raw', 'raw_write', 0, False, "Enable raw writing; requires knowing track size"), ('-multi', 'multi_session', 0, False, "Fixate sessions to allow later sessions; may conflict with DAO"), ('-msinfo', 'msinfo', 0, False, "Used internally to retrieve Nth session info for mkisofs"), ('-fix', 'fixate', 0, False, "Fixate a previously written but unfixated disc"), ('-nofix', 'nofixate', 0, False, "Don't fixate after writing tracks; for step-by-step audio"), ('-waiti', 'wait_for_input', 0, False, "Wait for input before opening the SCSI driver; for Nth session"), ('-eject', 'eject', 0, False, "Eject the disc after the current operation"), ('-checkdrive', 'checkdrive', 0, False, "Check for existence, knowledge of drive; return 0 if so"), ('-scanbus', 'scanbus', 0, False, "Scan all devices on all SCSI buses and print -inq info"), ('-overburn', 'overburn', 0, False, "Allow burns over the official medium size. May require DAO, or may fail"), ('-pad', 'pad', 0, True, "Pad tracks to allowed sizes"), ('-useinfo', 'useinfo', 0, False, "Use *.inf files to specify audio options"), ('-vv', 'verbose', 0, True, "Increase verbosity of cdrecord. Risks underruns"), ('speed=%s', 'speed', 1, None, "Set the speed. On recent MCC drives, defaults to max"), ('blank=%s', 'blanktype', 1, None, "Blank a CD-RW one of {help,all,fast,track,unreserve,trtail,unclose,session}"), ('dev=%s', 'device', 1, None, "Set the CD-Recording device"), ('gracetime=%s', 'gracetime', 1, None, "Wait N seconds before starting to write. Must be >= 2"), ('driver=%s', 'driver', 1, None, "Override the driver; see driver=help for details"), ('driveropts=%s', 'driveropts', 1, None, "Set specific driver options; list with driveropts=help -checkdrive"), ('mcn=%s', 'media_catalog_number', 1, None, "Set the Media Catalog Number"), ('cuefile=%s', 'cuefile', 1, None, "Take recording information from a CDRWIN CUE file; requires DAO"), ('tsize=%s', 'track_size', 1, None, "Specify the track size, for DAO/SAO burns") ] def command(self): if self.binary is None: raise ValueError('%s could not be found in your $PATH' % self.binary_name) args = [self.binary] for arg, attr, takes_arg, default, description in self.params: if takes_arg: if hasattr(self, attr): args.append(arg % getattr(self, attr)) else: if hasattr(self, attr): if getattr(self, attr): args.append(arg) else: if default: args.append(arg) # files for track in self.tracks: args.extend(track.get_args()) return tuple(args) def get_devices(self): if self.binary is None: raise ValueError('%s could not be found in your $PATH' % self.binary_name) devs = [] for line in os.popen("%s -scanbus 2>/dev/null" % self.binary, "r"): if line.startswith('\t'): dev, desc = line.strip().split('\t') if "'" in desc: dev = dev.strip() idx, name, space1, info, space2, ser, desc = [ s.strip() for s in desc.split("'") ] devs.append(Device(dev, '%s: %s' % (name, info))) return tuple(devs) def get_capabilities(self, dev): if self.binary is None: raise ValueError('%s could not be found in your $PATH' % self.binary_name) return iter(os.popen('%s dev=%s -prcap 2>/dev/null' % (self.binary, dev), 'r')) def get_multisession_info(self): if self.binary is None: raise ValueError('%s could not be found in your $PATH' % self.binary_name) return os.popen('%s dev=%s -msinfo 2>/dev/null' % (self.binary, self.device), 'r').read().strip() class Device: def __init__(self, dev, description): self.dev = dev self.description = description self.caps = {} for line in cdrecord().get_capabilities(dev): if line == '': continue for match, cap in self.colon: idx = line.find(match) if idx >= 0: val = line[idx+len(match):] val = val.strip(' :\n') self.caps[cap] = val break else: for match, cap in self.is_: idx = line.find(match) if idx >= 0: if line.find('Is not') >= 0: self.caps[cap] = False break elif line.find('Is') >= 0: self.caps[cap] = True break else: for match, cap in self.does: idx = line.find(match) if idx >= 0: if line.find('Does not') >= 0: self.caps[cap] = False break elif line.find('Does') >= 0: self.caps[cap] = True break #__import__('pprint').pprint(self.caps) does = [ ('deliver composite A/V data', 'delivers_composite_av'), ('read CD-R media', 'reads_cdr'), ('read CD-RW media', 'reads_cdrw'), ('read DVD-ROM media', 'reads_dvdrom'), ('read DVD-R media', 'reads_dvdr'), ('read DVD-RAM media', 'reads_dvdram'), ('read Mode 2 Form 1 blocks', 'reads_mode2form1'), ('read Mode 2 Form 2 blocks', 'reads_mode2form2'), ('read digital audio blocks', 'reads_digital_audio'), ('read multi-session CDs', 'reads_multisession'), ('read fixed-packet CD media using Method 2', 'reads_fixed_packet_2'), ('read CD bar code', 'reads_barcode'), ('read R-W subcode information', 'reads_rw_subcode'), ('read raw P-W subcode data from lead in', 'reads_pw_subcode'), ('write CD-R media', 'writes_cdr'), ('write CD-RW media', 'writes_cdrw'), ('write DVD-R media', 'writes_dvdr'), ('write DVD-RAM media', 'writes_dvdram'), ('allow media to be locked in the drive via PREVENT/ALLOW command', 'allows_lock_via_prevent'), ('have valid data on falling edge of clock', 'has_valid_on_falling'), ('have load-empty-slot-in-changer feature', 'has_changer_load_empty'), ('lock media on power up via prevent jumper', 'locks_via_prevent'), ('play audio CDs', 'plays_audio_cd'), ('restart non-streamed digital audio reads accurately', 'restarts_audio_accurately'), ('return R-W subcode de-interleaved and error-corrected', 'returns_rw_subcode_corrected'), ('return CD media catalog number', 'returns_cd_mcn'), ('return CD ISRC information', 'returns_isrc_info'), ('send digital data LSB-first', 'sends_data_LSB_first'), ('set LRCK high for left-channel data', 'sets_lrck_high_on_left'), ('support test writing', 'supports_test'), ('support Buffer-Underrun-Free recording', 'supports_burnfree'), ('support C2 error pointers', 'supports_c2_pointers'), ('support individual volume control setting for each channel', 'supports_channel_volume'), ('support independent mute setting for each channel', 'supports_channel_mute'), ('support digital output on port 1', 'supports_do_port1'), ('support digital output on port 2', 'supports_do_port2'), ('support ejection of CD via START/STOP command', 'supports_eject'), ('support changing side of disk', 'supports_side_change'), ('support Individual Disk Present feature', 'supports_disk_present'), ] is_ = [ ('currently in a media-locked state', 'is_locked') ] colon = [ ('scsidev', 'scsidev'), ('Linux sg driver version', 'sg_driver_version'), ('Device type', 'device_type'), ('Version', 'device_version'), ('Response Format', 'response_format'), ('Vendor_info', 'vendor_info'), ('Identifikation', 'device_identification'), ('Revision', 'device_revision'), ('Device seems to be', 'device_seems'), ('Number of volume control levels', 'volume_control_levels'), ('Length of data in BCLKs', 'bclk_data_length'), ('Maximum read speed', 'max_read'), ('Current read speed', 'current_read'), ('Maximum write speed', 'max_write'), ('Current write speed', 'current_write'), ('Rotational control selected', 'rotational_control'), ('Buffer size in KB', 'buffer_size_kb'), ('Copy management revision supported', 'supported_copy_management'), ('Number of supported write speeds', 'supported_write_speeds'), ] class Track: def __init__(self, filename): self.filename = filename class AudioTrack(Track): def get_args(self): return ('-audio', self.filename) class DataTrack(Track): def get_args(self): return ('-data', self.filename) class mkisofs(program): def __init__(self): self.binary_name = 'mkisofs' try: self.binary = self.findprogram('mkisofs') except ValueError: self.binary = None self.files = [] self.rock_ridge = True params = [ ('-A%s', 'application_id', 1, 128, None, "Describe this disc's application"), ('-b%s', 'eltorito_image', 1, None, None, "Pathname of 1200, 1440, or 2880 kB image"), ('-C%s', 'multisession_info', 1, None, None, 'Session start information from "cdrecord -msinfo"'), ('-P%s', 'publisher_id', 1, 128, None, "Publisher of the disc, usually with mail address and phone"), ('-p%s', 'preparer_id', 1, 128, None, "Preparer of the disc, usually with mail address and phone"), ('-V%s', 'volume_id', 1, 32, None, "Volume name / label; mount point for Solaris"), ('-M%s', 'merge_session_path', 1, None, None, "Path to the existing sesion for inclusive multi-session"), ('-o%s', 'output_filename', 1, None, None, "Output filename for saving ISO image"), ('-J', 'joliet_records', 0, None, True, "Generate Joliet directory records, limited to 64 UTF-16 chars"), ('-f', 'follow_symlinks', 0, None, True, "Follow symbolic links; otherwise Rock Ridge links, or ignored"), ('-l', 'thirtyone_chars', 0, None, False, "Allow 31 character filenames; bad for MS-DOS compatibility"), ('-L', 'leading_dot', 0, None, False, "Allow filenames to begin with a period instead of underscore"), ('-print-size', 'print_size', 0, None, False, "Used internally to detect size for TAO burns"), ('-R', 'rock_ridge_direct_perms', 0, None, False, "Use Rock Ridge format with permissions as on files"), ('-r', 'rock_ridge', 0, None, True, "Use Rock Ridge format with sanitized (uid=0 755/644) permissions"), ] def command(self): if self.binary is None: raise ValueError('%s could not be found in your $PATH' % self.binary_name) args = [self.binary] for arg, attr, takes_arg, length, default, description in self.params: if takes_arg: if hasattr(self, attr): args.append(arg % getattr(self, attr)) else: if hasattr(self, attr): if getattr(self, attr): args.append(arg) else: if default: args.append(arg) args.append('-graft-points') args.append('-gui') # files args.append('--') for fname, forig in self.files: args.append('%s=%s' % (fname.replace('\\', '\\\\').replace('=', '\\='), forig.replace('\\', '\\\\').replace('=', '\\='))) return tuple(args) def get_image_size(self): self.print_size = True out = self.run_for_stdout() out.strip() del self.print_size try: return int(out) * 2048 except ValueError: raise RuntimeError('Unexpected size reporting: "%s"' % out) def nb_read(fileno): import select r,w,x = select.select([fileno], [], [], 0.0) if len(r) == 0: return '' return os.read(fileno, 16384) def nb_write(fileno, buf): import select r,w,x = select.select([], [fileno], [], 0.0) if len(w) == 0: return 0 return os.write(fileno, buf) class Session: app = None iso = None cdrecord = None def next_session(self): gtk.idle_add(self.app.burn_next_session) def abort_burn(self): gtk.idle_add(self.app.abort_burn) try: err = self.iso.stderr_last self.app.error('Possible mkisofs error: %s' % err) except AttributeError: pass try: err = self.cdrecord.stderr_last self.app.error('Possible cdrercord error: %s' % err) except AttributeError: pass def getlines(self, fileno, buffer, newline='\n'): """Return (lines_array, remainder)""" buffer += nb_read(fileno) lines = [] while 1: idxs = [buffer.find(c) for c in newline] idxs = [idx for idx in idxs if idx >= 0] if len(idxs) == 0: break; idx = min(idxs) lines.append(buffer[:idx]) buffer = buffer[idx+1:] return lines, buffer def check_done(self): raise NotImplementedError def on_iso_stderr_read(self, source, condition): assert source == source # don't use this; block pychecker warns iso = self.iso #if condition != gtk.gdk.INPUT_READ: if condition & 1: lines, iso.stderr_buf = \ self.getlines(iso.stderr.fileno(), iso.stderr_buf) for line in lines: index = line.find('% done, estimate finish') if index < 0: self.app.helper_log(self.app.helper_log_mkisofs, line, 'stderr') iso.stderr_last = line else: frac = float(line[:index]) / 100 self.app.set_mkisofs_info(fraction=frac) elif condition & 16: gtk.input_remove(iso.stderr_id) iso.stderr.close() self.app.set_mkisofs_info(message='mkisofs: idle', fraction=1.0) iso.done = 1 iso.exitval = iso.poll() >> 8 self.check_done() else: self.app.error("Unexpected condition on mkisofs.stderr.read: %d" % condition) return True # call me again def on_cdr_stdout_read(self, source, condition): assert source == source # don't use this; block pychecker warns cdr = self.cdrecord #if condition & gtk.gdk.INPUT_READ: return if condition & 1: lines, cdr.stdout_buf = \ self.getlines(cdr.stdout.fileno(), cdr.stdout_buf, newline='\r\n\x08') for line in lines: if line == '': continue for info in ('seconds.', 'MB written', 'Fixating', 'Writing '): index = line.find(info) if index < 0: continue if info == 'seconds.': try: delay = int(line[index-3:index]) except ValueError: delay = '...' self.app.set_cdrecord_info(message='waiting %s' % delay) break elif info == 'MB written': # Track NN: xxxx MB written (fifo fff%) [buf bbb%] ss.sx." # Track NN: xxxx of yyyy MB written (fifo fff%) [buf bbb%] ss.sx." try: origline = line mb = speed = -1 # default values colon = line.find(':') tracknum = int(line[5:colon]) line = line[colon+1:] MB = line.find('MB written') of = line.find('of') percentage = None if 0 <= MB: if 0 <= of: mb = int(line[:of]) mb_to_go = int(line[of+2:MB]) percentage = float(mb)/mb_to_go else: mb = int(line[:MB]) try: percentage = mb/self.iso.sizemb except AttributeError: percentage=None line = line[MB+1:] fifo = line.find('(fifo') + 5 percent = line.find('%)') if 0 <= fifo < percent: fifopercent = int(line[fifo:percent]) line = line[percent+1:] else: fifopercent = None buf = line.find('[buf')+4 percent = line.find('%]') if 0 <= buf < percent: bufpercent = int(line[buf:percent]) line = line[percent+2:] else: bufpercent = None x = line.find('x.') if 0 <= x: speed = float(line[:x]) self.app.set_cdrecord_info(message= 'cdrecord: wrote %dMB at %sx' % (mb, speed), fraction=percentage, fifo=fifopercent, buffer=bufpercent) except ValueError: self.app.warn( 'ValueError parsing cdrecord output line "%s"' % origline) pass break elif info == 'Writing ': try: time = float(line[line.index(':')+1:-1]) except ValueError: pass else: cdr.write_time = time self.app.info('Burnt session in %.2fs' % time) elif info == 'Fixating': if line.find('...') >= 0: self.app.set_cdrecord_info(message= 'cdrecord: Fixating...') break try: time = float(line[line.index(':')+1:-1]) except ValueError: pass else: cdr.fixate_time = time self.app.info('Fixated session in %.2fs' % time) break else: self.app.helper_log(self.app.helper_log_cdrecord, line, 'stdout') cdr.stdout_last = line #elif condition & gtk.gdk.INPUT_EXCEPTION: elif condition & 16: if not cdr.done: cdr.done = 1 gtk.input_remove(cdr.stdout_id) gtk.input_remove(cdr.stdoutexc_id) gtk.input_remove(cdr.stderr_id) #gtk.input_remove(cdr.stdin_id) cdr.stdout.close() cdr.stderr.close() #total_time = cdr.write_time + cdr.fixate_time self.app.set_cdrecord_info(message='cdrecord: idle', fraction=0.0) cdr.exitval = cdr.poll() >> 8 self.check_done() else: self.app.error('InternalError: cdr.out: condition %s' % condition) return True # call me again def on_cdr_stderr_read(self, source, condition): assert source == source # don't use this; block pychecker warns cdr = self.cdrecord #if condition & gtk.gdk.INPUT_READ: return if condition & 1: lines, cdr.stderr_buf = \ self.getlines(cdr.stderr.fileno(), cdr.stderr_buf) for line in lines: if line.find('Drive needs to reload the media to return to proper status.') != -1: self.app.set_cdrecord_info( message='cdrecord: Reloading Media...') self.app.info('Reloading media to return drive to proper status') elif line.find('This media cannot be written in streaming mode anymore.') != -1: self.app.set_cdrecord_info( messages='cdrecord: Unwritable Disc') self.app.info('If CDRW media, try blanking it first.') self.app.info('Disc is already recorded') self.app.helper_log(self.app.helper_log_cdrecord, line, 'stderr') cdr.stderr_last = line elif condition & 16: if not cdr.done: cdr.done = 1 gtk.input_remove(cdr.stdout_id) gtk.input_remove(cdr.stdoutexc_id) gtk.input_remove(cdr.stderr_id) cdr.stdout.close() cdr.stderr.close() self.app.set_cdrecord_info(message='cdrecord: idle', fraction=1.0) cdr.exitval = cdr.poll() >> 8 self.check_done() else: self.app.error('InternalError: cdr.err: condition %s' % condition) return True # call me again class DataSession(Session): def __init__(self, app): self.app = app self.iso = mkisofs() self.cdrecord = cdrecord() self.mount = mount() def start(self): iso = self.iso cdr = self.cdrecord try: msinfo = iso.multisession_info except AttributeError: pass else: if cdr is msinfo: iso.multisession_info = cdr.get_multisession_info() # ensure size isn't out of date due to msinfo try: iso.size = iso.get_image_size() except Exception, exc: self.app.error("Error getting track size.") self.app.abort_burn(str(exc)) iso.sizemb = iso.size / 2.0**20 if hasattr(cdr, 'track_size'): cdr.track_size = str(iso.size / 2048) + 's' iso.done = 0 app.set_mkisofs_info(message='mkisofs', fraction=0.0) app.set_cdrecord_info(message='cdrecord', fraction=0.0) iso_cdr = os.pipe() iso.execute(stdout=iso_cdr) iso.stdin.close() iso.stderr_id = gtk.input_add(iso.stderr, gtk.gdk.INPUT_READ, self.on_iso_stderr_read) cdr.execute(stdin=iso_cdr) cdr.stdout_id = gtk.input_add(cdr.stdout, gtk.gdk.INPUT_READ, self.on_cdr_stdout_read) cdr.stdoutexc_id = gtk.input_add(cdr.stdout, gtk.gdk.INPUT_READ, self.on_cdr_stdout_read) cdr.stderr_id = gtk.input_add(cdr.stderr, gtk.gdk.INPUT_READ, self.on_cdr_stderr_read) def check_done(self): if self.iso.done and self.cdrecord.done: if self.iso.exitval == 0 and self.cdrecord.exitval == 0: if self.verify(): self.app.set_cdrecord_info(message='cdrecordi idle') self.next_session() else: self.app.warn("Verification Failed") self.abort_burn() else: self.abort_burn() def verify(self): # don't verify when not requested mount = self.cdrecord.verify if mount == '': return True # Mount or die if self.mount.mount(mount) != 0: self.app.error("Couldn't mount %s for verification" % mount) return False try: d = { 'vsize': 0, 'errors': False, 'mount': mount } for dest, source in self.iso.files: d['dest'] = dest d['source'] = source if os.path.isdir(source): self.app.info("Verifying %s%s ..." % (dest, os.sep)) os.path.walk(source, self.verify_walk, d) else: self.app.info("Verifying %s ..." % dest) self.verify_path(d, os.path.join(mount, dest), source) finally: # Unmount or complain if self.mount.unmount(mount) != 0: self.app.error("Couldn't unmount %s after verification" % mount) if d['errors']: return False self.app.info("Verification Succeeded") return True def verify_walk(self, d, dirname, fnames): for f in fnames: sourcename = os.path.join(dirname, f) if os.path.isdir(sourcename): continue destname = os.path.join(mount, dest, dirname[len(d['source'])+1:], f) self.verify_path(d, destname, sourcename) def verify_path(self, d, destname, sourcename): BUFSIZE = 2**14 CHECKMASK = 2**20 - 1 fsize = 0 fread = 0 fitname = destname[-32:] if fitname != destname: fitname = '...' + destname[-30:] self.app.set_cdrecord_info(message='verify: %s' % fitname) while gtk.events_pending(): gtk.main_iteration() try: fa = os.open(sourcename, os.O_RDONLY) except OSError, err: self.app.error("Couldn't open source file for comparison: %s" % str(err)) d['errors'] = True return try: fsize = os.stat(sourcename)[6] except OSError: pass try: try: fb = os.open(destname, os.O_RDONLY) except OSError, err: self.app.error("Couldn't open dest file for comparison: %s" % str(err)) d['errors'] = True return try: while 1: try: a = os.read(fa, BUFSIZE) b = os.read(fb, BUFSIZE) except (IOError, OSError), err: self.app.error("Error reading file for comparison: %s" % str(err)) d['errors'] = True if a != b: self.app.error("File %s differs from source" % destname) d['errors'] = True break elif len(a) == 0: break fread += len(a) if not (fread & CHECKMASK): while gtk.events_pending(): gtk.main_iteration() self.app.set_cdrecord_info(fraction=float(d['vsize']+fread)/self.iso.size) finally: try: os.close(fb) except: pass finally: try: os.close(fa) except: pass d['vsize'] += fsize class AudioSession(Session): def __init__(self, app): self.app = app self.cdrecord = cdrecord() def start(self): cdr = self.cdrecord cdr.execute() cdr.stdout_id = gtk.input_add(cdr.stdout, gtk.gdk.INPUT_READ, self.on_cdr_stdout_read) cdr.stdoutexc_id = gtk.input_add(cdr.stdout, gtk.gdk.INPUT_READ, self.on_cdr_stdout_read) cdr.stderr_id = gtk.input_add(cdr.stderr, gtk.gdk.INPUT_READ, self.on_cdr_stderr_read) def check_done(self): if self.cdrecord.done: if self.cdrecord.exitval == 0: self.next_session() else: self.abort_burn() def filesize(bytes): cutoffs = ((10, 'K'), (20, 'M'), (30, 'G'), (40, 'T')) if bytes < 1000: return str(bytes) + 'B' for exp, suffix in cutoffs: if bytes < 1000*2**exp: size = bytes/2.0**exp if size < 10: return '%.2f%s' % (size, suffix) else: return '%.1f%s' % (size, suffix) else: return 'Way too big' class LayoutRowType: icon = None is_session = False is_container = False is_audio_container = False is_file_container = False is_directory_container = False is_file = False is_directory = False is_sized = False is_removable = True is_root = False popup = None def __init__(self, app, view, name, size, source=None): self.app = app self.view = view self.name = name self.size = size self.source = source if not isinstance(self.icon, gtk.gdk.Pixbuf): # Load Icon v = self.app.pixbuf[self.icon] if v[2] is None: stockid, size = v[0].get_stock() icon = view.render_icon(stockid, size) else: icon = v[2].render_icon(view.get_style(), gtk.TEXT_DIR_NONE, gtk.STATE_NORMAL, gtk.ICON_SIZE_MENU, view, None) self.__class__.icon = icon if self.popup is not None and not isinstance(self.popup, gtk.Menu): # generate menu menu = gtk.Menu() #__import__('pprint').pprint(self.popup) view.populate_submenu(menu, self.popup) self.popup = menu def get_size(self): return self.size def add_size(self, iter, size): return self.set_size(iter, self.size + size) def set_size(self, iter, size): self.size = size self.view.model.row_changed(self.view.model.get_path(iter), iter) def get_name(self): return self.name def set_name(self, iter, name): self.name = name self.view.model.row_changed(self.view.model.get_path(iter), iter) def __str__(self): return '%s %s' % (self.__class__.__name__, repr(self.get_name())) def __repr__(self): return '<%s %s=%s>' % (self.__class__.__name__, repr(self.get_name()), repr(self.source)) class LayoutRoot(LayoutRowType): is_removable = False is_root = True class LayoutCD(LayoutRoot): icon = 'cdrom' is_container = True popup = [ ('burn_device', 'Burning Device:', 'devices', None), '----', ('verify_device', 'Verify Device:', 'mounts', None), '----', ('burn_speed', 'Burn Speed', 'submenu', [ ('burn_speed', 'Maximum', 'radio', -1), ('burn_speed', '52x', 'radio', 52), ('burn_speed', '48x', 'radio', 48), ('burn_speed', '40x', 'radio', 40), ('burn_speed', '32x', 'radio', 32), ('burn_speed', '24x', 'radio', 24), ('burn_speed', '16x', 'radio', 16), ('burn_speed', '12x', 'radio', 12), ('burn_speed', '8x', 'radio', 8), ('burn_speed', '4x', 'radio', 4), ('burn_speed', '2x', 'radio', 2), ('burn_speed', '1x', 'radio', 1), ('burn_speed', 'Minimum', 'radio', 0), ]), ('burn_test', 'Simulate Burn (no laser)', 'check', False), ('burn_free', 'Burnproof', 'check', True), ('burn_sao', 'Session At Once (SAO)', 'check', False), ('burn_over', 'Overburn (past official size)', 'check', False), ] class LayoutSession(LayoutRowType): is_session = True # must implement this if self.is_session is true def create_session(self, it, (session_num, session_count)): raise NotImplementedError def set_cdrecord_options(self, cdrecord, iso=None): cdview = self.view try: dev = cdrecord.device = cdview.burn_device except AttributeError: if hasattr(cdvew, 'all_devieces') and len(cdview.all_devices) > 0: raise ValueError("No selected burning device") else: raise ValueError("No burning devices are available") try: cdrecord.verify = cdview.verify_device except AttributeError: cdrecord.verify = '' device = cdview.all_devices[dev] if cdview.burn_speed != -1: cdrecord.speed = cdview.burn_speed if cdview.burn_free: if device.caps['supports_burnfree']: cdrecord.driveropts = 'burnfree' else: self.app.warn("%s doesn't support Burnproof. Burnproof Disabled" % device.description) if cdview.burn_test: if not device.caps['supports_test']: raise ValueError("%s does not support test writing." " Burn aborted" % device.description) cdrecord.test_burn = True cdrecord.verify = '' # don't verify unwritten data cdrecord.nofixate = True # some drives don't like -dummy -fix cdrecord.fixate = False if cdview.burn_sao: cdrecord.session_at_once = True if iso is not None: # audio CDs have no ISO; XXX can they do sao? cdrecord.track_size = str(iso.size / 2048) + 's' if cdview.burn_over: cdrecord.overburn = True class LayoutAudioSession(LayoutSession): icon = 'audio_session' is_container = True is_audio_container = True def create_session(self, session_iter, (session_num, session_count)): view = self.view tree = view.model session = AudioSession(self.app) cdr = session.cdrecord self.set_cdrecord_options(cdr) if session_num < session_count: cdr.multi_session = True # set up cdrecord it = tree.iter_children(session_iter) while it is not None: assert not tree.iter_has_child(it) rowobj = tree.get_value(it, 0) cdr.tracks.append(AudioTrack(rowobj.source)) it = tree.iter_next(it) self.app.info("+ Audio session with %d tracks" % len(cdr.tracks)) return session class LayoutDataSession(LayoutSession): icon = 'data_session' is_container = True is_file_container = True is_directory_container = True is_sized = True def create_session(self, session_iter, (session_num, session_count)): view = self.view tree = view.model session = DataSession(self.app) iso = session.iso cdr = session.cdrecord rowobj = tree.get_value(session_iter, 0) # set up mkisofs iso.volume_id = rowobj.get_name() iters = [ tree.iter_children(session_iter) ] path = [ os.sep ] if iters[0] == None: raise ValueError("Empty Data Session") addfile = iso.files.append while len(iters): rowobj = tree.get_value(iters[-1], 0) if rowobj.is_file or rowobj.is_directory: pathname = os.path.join(*path[1:] + [rowobj.get_name()]) addfile((pathname, rowobj.source)) if rowobj.is_container and tree.has_children(iters[-1]): path.append(rowobj.get_name()) iters.append(tree.iter_children(iters[-1])) else: while len(iters): next = tree.iter_next(iters[-1]) if next is None: iters.pop() path.pop() else: iters[-1] = next break try: iso.size = iso.get_image_size() except Exception, exc: self.app.error("Error getting track size") raise ValueError(str(exc)) iso.sizemb = iso.size / 2.0**20 if session_num > 1: iso.multisession_info = cdr # set up cdrecord self.set_cdrecord_options(cdr, iso) cdr.tracks.append(DataTrack('-')) if session_num < session_count: cdr.multi_session = True self.app.info("+ Data session with %d specified files" % len(iso.files)) return session class LayoutVirtualDirectory(LayoutRowType): icon = 'virtual_dir' is_container = True is_file_container = True is_directory_container = True is_sized = True class LayoutStaticDirectory(LayoutRowType): icon = 'directory' is_directory = True is_sized = True class LayoutFile(LayoutRowType): icon = 'data_file' is_file = True is_sized = True class LayoutAudioTrack(LayoutRowType): icon = 'audio_track' is_file = True is_sized = True class CDView(gtk.TreeView): # ColName, Field, Type, Renderer columnmodel = [ ("Edit", 0, gobject.TYPE_BOOLEAN), ("Icon", 1, gtk.gdk.Pixbuf), ("File", 2, gobject.TYPE_STRING), ("Size", 3, gobject.TYPE_UINT64), ("Type", 4, gobject.TYPE_STRING), ("Orig", 5, gobject.TYPE_STRING), ] column = dict([ (s,i) for s,i,t in columnmodel ]) dest_targets = [('text/uri-list', 0, 0)] def __init__(self, app, *args, **kwargs): self.app = app gtk.TreeView.__init__(self, *args, **kwargs) self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) #self.model = gtk.TreeStore(*[t for s,i,t in self.columnmodel]) self.model = gtk.TreeStore(gobject.TYPE_PYOBJECT, gobject.TYPE_BOOLEAN) self.set_model(self.model) self.model.connect_after('row-inserted', self.on_row_inserted) self.model.connect('row-deleted', self.on_row_deleted) def render_icon(column, cell, model, iter, renderer): rowobj = model.get_value(iter, 0) renderer.set_property('pixbuf', rowobj.icon) def render_name(column, cell, model, iter, renderer): rowobj = model.get_value(iter, 0) renderer.set_property('text', rowobj.get_name()) def render_filesize(column, cell, model, iter, renderer): assert cell == cell # don't use this; block pychecker warns assert column == column rowobj = model.get_value(iter, 0) size = rowobj.get_size() if rowobj.is_container and size == 0: sizestr = '' else: sizestr = filesize(size) if rowobj.is_container: sizestr = sizestr.join('()') else: sizestr = sizestr.join(' ') renderer.set_property('text', sizestr) # View: Icon+File col = gtk.TreeViewColumn() col.set_title('File') r_p = gtk.CellRendererPixbuf() col.pack_start(r_p, expand=False) r_t = gtk.CellRendererText() col.pack_start(r_t, expand=True) col.add_attribute(r_t, 'editable', 1) r_t.connect('edited', self.on_edit_file, self.model) #col.set_resizable(True) col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) col.set_cell_data_func(r_p, render_icon, r_p) col.set_cell_data_func(r_t, render_name, r_t) self.append_column(col) self.set_expander_column(col) # View: Size col = gtk.TreeViewColumn() col.set_title('Size') col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) r_t = gtk.CellRendererText() r_t.set_property('xalign', 1.0) col.pack_start(r_t, expand=False) col.set_cell_data_func(r_t, render_filesize, r_t) self.append_column(col) #self.insert_column(col, 0) # connect signals self.connect('button-press-event', self.on_button_press_event) self.connect('key-press-event', self.on_key_press_event) self.set_reorderable(True) # start it with the CD self.model.append(None, row=(LayoutCD(self.app, self, 'CD Layout', 0), False)) def populate_submenu(self, menu, entries): def toggled(item, cdview, attr, val=None): """glorified lambda for the menu items, sets cdview.attr to val or item.get_active()""" if val is None: val = item.get_active() setattr(cdview, attr, val) for attr, title, instance, data in entries: if instance == '-': item = gtk.SeparatorMenuItem() elif instance == 'check': item = gtk.CheckMenuItem(title) item.set_active(data) toggled(item, self, attr) # set it to initial item.connect('toggled', toggled, self, attr) elif instance == 'radio': groupattr = 'property_%s_group' % attr try: group = getattr(self, groupattr) except AttributeError: group = None item = gtk.RadioMenuItem(group, title) if group is None: setattr(self, groupattr, item.get_group()[0]) item.set_active(True) toggled(item, self, attr, data) item.connect('toggled', toggled, self, attr, data) elif instance == 'submenu': item = gtk.MenuItem(title) item.show() submenu = gtk.Menu() self.populate_submenu(submenu, data) item.set_submenu(submenu) elif instance == 'devices': item = gtk.MenuItem(title) item.set_sensitive(False) item.show() menu.append(item) try: devices = cdrecord().get_devices() except ValueError, err: self.app.error(str(err)) self.app.warn("No devices are available") continue writers = [dev for dev in devices if dev.caps.get('writes_cdr') ] self.all_devices = dict([(dev.dev, dev) for dev in devices]) group = None if len(writers) == 0: self.app.warn("No CD Writers are available") for dev in writers: item = gtk.RadioMenuItem(group, dev.description) if group is None: item.set_active(True) group = item toggled(item, self, attr, dev.dev) item.connect('toggled', toggled, self, attr, dev.dev) item.show() menu.append(item) continue # already packed and shown elif instance == 'mounts': item = gtk.MenuItem(title) item.set_sensitive(False) item.show() menu.append(item) try: fstab = file('/etc/fstab') except OSError: continue group = item = gtk.RadioMenuItem(None, 'No Verification') item.show() menu.append(item) item.set_active(True) toggled(item, self, attr, '') item.connect('toggled', toggled, self, attr, '') for line in fstab: line = line.strip() if line.startswith('#'): continue try: dev, mount, type_, opts, dump, pass_ = line.split() except ValueError: continue if '/fd' in dev: continue if type_.lower() not in ('iso9660', 'auto'): continue if os.getuid() and ('user' not in opts.lower() or 'nouser' in opts.lower()): continue item = gtk.RadioMenuItem(group, '%s (%s)' % (mount, dev)) item.connect('toggled', toggled, self, attr, mount) item.show() menu.append(item) continue else: self.app.error("InternalError: Unexpected popup item instance '%s'" % instance) item.show() menu.append(item) # callbacks def on_edit_file(self, render, path, new, model=None): assert render == render # don't use this; block pychecker warns try: it = model.get_iter(path) # catch deleted-out rowobj = model.get_value(it, 0) rowobj.set_name(it, new) except ValueError: pass def on_key_press_event(self, view, event): assert view == view # don't use this; block pychecker warns if (gtk.gdk.keyval_name(event.keyval) == 'Delete' and self.selected_abilities()['remove']): self.remove_selected() def on_button_press_event(self, treeview, event): # popup cdview_popup on RMB if event.button == 3: x = int(event.x) y = int(event.y) time = gtk.get_current_event_time() try: path, col, cellx, celly = treeview.get_path_at_pos(x, y) except TypeError: return 0 treeview.grab_focus() self.cdview_popup_path = path it = self.model.get_iter(path) rowobj = self.model.get_value(it, 0) if rowobj.popup is not None: treeview.set_cursor(path, col, False) rowobj.popup.popup(None, None, None, event.button, time) return 1 return 0 def on_row_inserted(self, model, path, iter): assert iter == iter # don't use this; block pychecker warns path = path[:-1] gtk.idle_add(self.recalculate_path_size, model, path) def on_row_deleted(self, model, path): path = path[:-1] gtk.idle_add(self.recalculate_path_size, model, path) def recalculate_path_size(self, model, path): if len(path) < 1: return iter = model.get_iter(path) rowobj = model.get_value(iter, 0) prev_size = rowobj.get_size() calc_size = 0 child = model.iter_children(iter) while child is not None: calc_size += model.get_value(child, 0).get_size() child = model.iter_next(child) add_size = calc_size - prev_size rowobj.set_size(iter, calc_size) while add_size != 0: iter = model.iter_parent(iter) rowobj = model.get_value(iter, 0) if rowobj.is_sized: rowobj.add_size(iter, add_size) else: break def add_audio_session(self): model = self.model it = model.get_iter_root() session_iter = model.append(it, row=(LayoutAudioSession(self.app, self, 'Audio Session', 0), False)) session_path = model.get_path(session_iter) self.expand_row(self.model.get_path(it), False) column = self.get_column(0) gtk.idle_add(self.grab_focus) self.set_cursor(session_path, column, False) def add_data_session(self): model = self.model it = model.get_iter_root() session_iter = model.append(it, row=(LayoutDataSession(self.app, self, 'Data Session', 0), True)) session_path = model.get_path(session_iter) self.expand_row(self.model.get_path(it), False) column = self.get_column(0) #self.set_cursor(session_path, column, True) gtk.idle_add(self.grab_focus) gtk.idle_add(self.set_cursor, session_path, column, True) def add_subdir(self, it=None): if it is None: it = self.get_selected_iters()[0] model = self.model subdir_iter = model.append(it, row=(LayoutVirtualDirectory(self.app, self, 'New Directory', 0), True)) subdir_path = model.get_path(subdir_iter) self.expand_row(model.get_path(it), False) column = self.get_column(0) #self.set_cursor(subdir_path, column, True) gtk.idle_add(self.grab_focus) gtk.idle_add(self.set_cursor, subdir_path, column, True) def add_files(self, it=None): if it is None: it = self.get_selected_iters()[0] parentobj = self.model.get_value(it, 0) target = parentobj.get_name() if parentobj.is_file_container: files = self.pick_files("Add File(s) to %s" % target) for f in files: # if isdir(f) / if isfile(f) try: f = os.readlink(f) except OSError, err: if err.errno != errno.EINVAL: raise if os.path.isdir(f): name = os.path.basename(os.path.normpath(f)) size = [0] #ctxid = self.statusbar.get_context_id() ctxid = 0 # Need to watch to make sure our iter doesn't go out of date parent = [it, self.model.get_path(it)] def row_deleted(model, path, parent): if path == parent[1][:len(path)]: parent[0] = None elif (path[:-1] == parent[1][:len(path)-1] and path < parent[1]): # deleted preceding row; decrement path parent[1] = parent[1][:len(path)-1] + (parent[1][len(path)-1]-1,) + parent[1][len(path):] def row_inserted(model, path, it, parent): assert it == it # don't use this; block pychecker warns assert model == model if (path[:-1] == parent[1][:len(path)-1] and path <= parent[1][:len(path)]): # inserted preceding row; increment path parent[1] = parent[1][:len(path)-1] + (parent[1][len(path)-1]+1,) + parent[1][len(path):] # count on python to read row_deleted(), and then overwrite # row_deleted with the handler_id for later disconnect row_deleted = self.model.connect('row-deleted', row_deleted, parent) row_inserted = self.model.connect('row-inserted', row_inserted, parent) self.app.statusbar.push(ctxid, 'Reading...') os.path.walk(f, self.countsize, (size, ctxid, parent)) self.app.statusbar.pop(ctxid) self.model.disconnect(row_deleted) self.model.disconnect(row_inserted) it = parent[0] size = size[0] if it is None: self.app.warn("Moved or Deleted '%s'; Canceled Add '%s'" % (target, f)) else: self.model.append(it, row=(LayoutStaticDirectory(self.app, self, name, size, f), True)) else: name = os.path.basename(f) size = os.lstat(f)[stat.ST_SIZE] self.model.append(it, row=(LayoutFile(self.app, self, name, size, f), True)) elif parentobj.is_audio_container: files = self.pick_files("Add File(s) to %s" % target, '*.wav') for f in files: # if isdir(f) / if isfile(f) try: f = os.readlink(f) except OSError, err: if err.errno != errno.EINVAL: raise name = os.path.basename(f) size = os.lstat(f)[stat.ST_SIZE] self.model.append(it, row=(LayoutAudioTrack(self.app, self, name, size, f), False)) else: raise RuntimeError("Can't add files to %s" % str(parentobj)) if it is not None: self.expand_row(self.model.get_path(it), False) def countsize(self, (size, ctxid, parent), dirname, fnames): if parent[0] is None: del fnames[:] return join = os.path.join self.app.statusbar.pop(ctxid) self.app.statusbar.push(ctxid, 'Reading... (%s so far)' % filesize(size[0])) while gtk.events_pending(): gtk.main_iteration() for f in fnames: fullname = join(dirname, f) if not os.path.isdir(fullname): size[0] += os.lstat(fullname)[stat.ST_SIZE] def pick_files(self, title=None, complete=None): filesel = gtk.FileSelection(title) filesel.hide_fileop_buttons() filesel.set_select_multiple(True) if complete is not None: filesel.complete(complete) filesel.show() status = filesel.run() filesel.hide() if status == gtk.RESPONSE_OK: return filesel.get_selections() else: return tuple() def get_selected_iters(self): def append_selected(model, path, it, iters): assert model == model # don't use this; block pychecker warns assert path == path iters.append(it) iters = [] sel = self.get_selection() sel.selected_foreach(append_selected, iters) return iters def selected_abilities(self): caps = {} selected = self.get_selected_iters() rowobjs = [ self.model.get_value(it, 0) for it in selected ] # can only add directories to single selection of data or vdir caps['adddir'] = len(rowobjs)==1 and rowobjs[0].is_directory_container # can only add files to single selection of a session or vdir caps['addfile'] = len(rowobjs)==1 and (rowobjs[0].is_file_container or rowobjs[0].is_audio_container) # can't remove the CD caps['remove'] = len(rowobjs) > 0 and \ len([o for o in rowobjs if o.is_removable]) == len(rowobjs) caps['prefs'] = len(rowobjs)==1 and rowobjs[0].popup is not None return caps def selected_prefs(self): time = gtk.get_current_event_time() rowobj = self.model.get_value(self.get_selected_iters()[0], 0) if rowobj.popup is not None: rowobj.popup.popup(None, None, None, 1, time) def remove_selected(self): path = self.model.get_path(self.get_selected_iters()[0]) while 1: selected = self.get_selected_iters() if len(selected) == 0: break self.model.remove(selected[0]) # try to find something sane to focus while 1: try: self.model.get_iter(path) except ValueError: if path[-1] > 0: path = path[:-1] + (path[-1]-1,) else: path = path[:-1] else: self.set_cursor(path, None, False) break class Redeye: pixbuf_sources = [ ('cdrom', None, 'gtk-cdrom'), ('audio_session', 'icon-s-a.png', 'gtk-cdrom'), ('audio_track', 'icon-f-a.png', 'gtk-italic'), ('data_session', 'icon-s-d.png', 'gtk-copy'), ('virtual_dir', 'icon-d.png', 'gtk-open'), ('directory', 'icon-d-f.png', 'gtk-open'), ('data_file', None, 'gtk-new'), ('add_file', None, 'gtk-add'), ('remove_file', None, 'gtk-remove'), ('burn_cd', None, 'gtk-execute'), ('prefs', None, 'gtk-preferences'), ('view_log', None, 'gtk-justify-left'), ] toolbar_items = [ ('button_add_data_session', 'Data Session', 'data_session', 'Add a new Data Session to your Layout'), ('button_add_audio_session', 'Audio Session', 'audio_session', 'Add a new Audio Session to your Layout'), '----', ('button_add_dir', 'Add Folder', 'virtual_dir', 'Add a new Folder to your Layout'), ('button_add_files', 'Add File(s)', 'add_file', 'Add Tracks or Files to your Layout'), ('button_remove_selected', 'Remove', 'remove_file', 'Remove Selected Files, Tracks, or Sessions from your Layout'), '----', ('button_prefs', 'Preferences', 'prefs', 'CD Recording Preferences'), '----', ('button_burn', 'Burn CD', 'burn_cd', 'Burn the CD described by your Layout'), #'----', #('button_info', 'Info', 'gtk-dialog-info', 'Insert a test info'), #('button_warn', 'Info', 'gtk-dialog-warning', 'Insert a test warning'), #('button_error', 'Info', 'gtk-dialog-error', 'Insert a test error'), ] #def on_button_info_clicked(self, button): # self.info('Test INFO') #def on_button_warn_clicked(self, button): # self.warn('Test WARN') #def on_button_error_clicked(self, button): # self.error('Test ERROR') def on_button_add_audio_session_clicked(self, button): assert button == button # don't use this; block pychecker warns self.cdview.add_audio_session() def on_button_add_data_session_clicked(self, button): assert button == button # don't use this; block pychecker warns self.cdview.add_data_session() def on_button_add_dir_clicked(self, button): assert button == button # don't use this; block pychecker warns self.cdview.add_subdir() def on_button_add_files_clicked(self, button): assert button == button # don't use this; block pychecker warns try: self.cdview.add_files() except OSError, err: if err.errno == errno.ENOENT: self.error('%s: %s' % (err.strerror, err.filename)) else: self.error(str(err)) except Exception, exc: import traceback print ",-------------------------------------------------------." print "| Notify of the following error |" % 'tortall' print "`-------------------------------------------------------'" traceback.print_exc() self.error(str(exc)) print "`-------------------------------------------------------'" def on_button_remove_selected_clicked(self, button): assert button == button # don't use this; block pychecker warns self.cdview.remove_selected() def on_button_burn_clicked(self, button): assert button == button # don't use this; block pychecker warns self.burn_layout() def on_button_prefs_clicked(self, button): assert button == button # don't use this; block pychecker warns self.cdview.selected_prefs() def on_messages_dialog_response(self, dialog, response): if response in (gtk.RESPONSE_DELETE_EVENT, gtk.RESPONSE_CLOSE): self.message('', error=-1) dialog.hide() elif response == dialog.RESPONSE_CLEAR: self.message('', error=None) else: self.error('InternalError: Unhandled messages dialog response %d' % response) return True def on_messages_dialog_close_or_delete_event(self, dialog, event=None): assert event == event # don't use this; block pychecker warns dialog.hide() return True def on_helper_log_response(self, dialog, response): if response in (gtk.RESPONSE_DELETE_EVENT, gtk.RESPONSE_CLOSE): dialog.hide() elif response == dialog.RESPONSE_CLEAR: for attr in ('helper_log_mkisofs', 'helper_log_cdrecord'): logview = getattr(self, attr) buf = logview.get_buffer() sob, eob = buf.get_bounds() buf.delete(sob, eob) else: self.error('InternalError: Unhandled helper log response %d' % response) return True def on_helper_log_close_or_delete_event(self, dialog, event=None): assert event == event # don't use this; block pychecker warns dialog.hide() return True def show_helper_log(self, *args): assert args == args # don't use this; block pychecker warns self.helper_log_dialog.present() def show_messages_dialog(self, *args): """Show the messages window, reset the notification button""" assert args == args # don't use this; block pychecker warns self.message('', error=-1) self.messages_dialog.present() def on_cdview_selection_changed(self, *args): assert args == args # don't use this; block pychecker warns caps = self.cdview.selected_abilities() self.button_add_dir.set_sensitive(caps['adddir']) self.button_add_files.set_sensitive(caps['addfile']) self.button_remove_selected.set_sensitive(caps['remove']) self.button_prefs.set_sensitive(caps['prefs']) def __init__(self): # main application icon if os.path.exists('icon-eye.png'): pixbuf = gtk.gdk.pixbuf_new_from_file('icon-eye.png') gtk.window_set_default_icon_list((pixbuf)) # render some icons self.pixbuf = {} for key, src, stockfallback in self.pixbuf_sources: if src and os.path.exists(src): pixbuf = gtk.gdk.pixbuf_new_from_file(src) iconset = gtk.IconSet(pixbuf) img_toolbar = gtk.Image() img_toolbar.set_from_icon_set(iconset, gtk.ICON_SIZE_LARGE_TOOLBAR) img_menu = gtk.Image() img_menu.set_from_icon_set(iconset, gtk.ICON_SIZE_MENU) else: iconset = None img_toolbar = gtk.image_new_from_stock(stockfallback, gtk.ICON_SIZE_LARGE_TOOLBAR) img_menu = gtk.image_new_from_stock(stockfallback, gtk.ICON_SIZE_MENU) self.pixbuf[key] = img_menu, img_toolbar, iconset # main window win = self.mainwin = gtk.Window() win.set_title("Redeye " + __version__) win.set_default_size(gtk.gdk.screen_width()/3,gtk.gdk.screen_height()/2) win.connect('delete-event', self.on_redeye_delete_event) win.connect('destroy-event', self.on_redeye_destroy_event) # hold everything in a vbox vbox = gtk.VBox() win.add(vbox) # precreate important items; don't pack yet self.create_helper_log_dialog() self.create_messages_dialog() # messages indicator, dialog button button = self.messages_button = gtk.Button() icon = button.icon_error = gtk.image_new_from_stock( 'gtk-dialog-error', gtk.ICON_SIZE_MENU) icon.show() icon.set_padding(0,0) icon = button.icon_warn = gtk.image_new_from_stock( 'gtk-dialog-warning', gtk.ICON_SIZE_MENU) icon.show() icon.set_padding(0,0) icon = button.icon_info = gtk.image_new_from_stock( 'gtk-dialog-info', gtk.ICON_SIZE_MENU) icon.show() icon.set_padding(0,0) button.add(icon) button.connect('clicked', self.show_messages_dialog) self.warn('', None) #menubar = gtk.MenuBar() #vbox.pack_start(menubar) # toolbar toolbar = gtk.Toolbar() vbox.pack_start(toolbar, expand=False) toolbar.set_style(gtk.TOOLBAR_ICONS) toolbar.set_tooltips(True) for attr, text, icon, tooltip in self.toolbar_items: if attr == '-': toolbar.append_space() continue callback = getattr(self, 'on_%s_clicked' % attr) tooltip_private = tooltip.join('()') if isinstance(icon, (str, unicode)): icon = self.pixbuf[icon][1] button = toolbar.append_item(text, tooltip, tooltip_private, icon, callback) setattr(self, attr, button) # cd construction self.cdview = CDView(self) self.cdview.get_selection().connect('changed', self.on_cdview_selection_changed) scrolledwin = gtk.ScrolledWindow() scrolledwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) scrolledwin.add(self.cdview) vbox.pack_start(scrolledwin) # progress feedback self.pbar_mkisofs = gtk.ProgressBar() self.pbar_mkisofs.set_text("mkisofs: idle") pbar = self.pbar_fifo = gtk.ProgressBar() pbar.set_text("fifo: 0%") #pbar.set_orientation(gtk.PROGRESS_BOTTOM_TO_TOP) pbar = self.pbar_buffer = gtk.ProgressBar() pbar.set_text("buffer: 0%") #pbar.set_orientation(gtk.PROGRESS_BOTTOM_TO_TOP) self.pbar_cdrecord = gtk.ProgressBar() self.pbar_cdrecord.set_text("cdrecord: idle") hbox = gtk.HBox() vbox.pack_start(hbox, expand=False) hbox.pack_start(self.pbar_mkisofs) hbox.pack_start(self.pbar_fifo) hbox.pack_start(self.pbar_buffer) vbox.pack_start(self.pbar_cdrecord, expand=False, fill=False) # status stuff hbox = gtk.HBox() vbox.pack_start(hbox, expand=False) self.statusbar = gtk.Statusbar() self.statusbar.set_has_resize_grip(False) hbox.pack_start(self.statusbar) # should this be here or toolbar? hmm. button = self.helper_log_button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) icon = gtk.image_new_from_stock('gtk-justify-left', gtk.ICON_SIZE_MENU) button.add(icon) button.connect('clicked', self.show_helper_log) hbox.pack_start(button, expand=False, fill=False) # pack messages button hbox.pack_start(self.messages_button, expand=False, fill=False) self.on_cdview_selection_changed() win.show_all() #self.pbar_mkisofs.hide() #self.pbar_cdrecord.hide() def create_messages_dialog(self): msgs = self.messages_dialog = \ gtk.Dialog(title='Redeye: Messages')#, parent=self.mainwin) msgs.set_default_size(400,300) label = gtk.Label('Messages:') label.set_padding(5, 3) label.set_alignment(0, 0) msgs.vbox.pack_start(label, expand=False) self.messages_model = gtk.ListStore(gtk.gdk.Pixbuf, str, float) view = self.messages_view = gtk.TreeView(self.messages_model) view.set_rules_hint(True) view.set_headers_visible(False) view.append_column( gtk.TreeViewColumn('', gtk.CellRendererPixbuf(), pixbuf=0)) view.append_column( gtk.TreeViewColumn('Time', gtk.CellRendererText(), text=2)) view.append_column( gtk.TreeViewColumn('Message', gtk.CellRendererText(), text=1)) # render the float as a time def render_hms(column, cell, model, iter): assert cell == cell # don't use this; block pychecker warns renderer = column.get_cell_renderers()[0] msg_time = model.get_value(iter, 2) renderer.set_property('text', time.strftime('%H:%M:%S', time.localtime(msg_time))) col = view.get_column(1) renderer = col.get_cell_renderers()[0] col.set_cell_data_func(renderer, render_hms) # scroll the warnings so they don't get too huge scrolledwin = gtk.ScrolledWindow() scrolledwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scrolledwin.add(view) msgs.RESPONSE_CLEAR = 1 msgs.add_button('gtk-clear', msgs.RESPONSE_CLEAR) msgs.add_button('gtk-close', gtk.RESPONSE_CLOSE) msgs.vbox.pack_start(scrolledwin) msgs.vbox.show_all() msgs.set_has_separator(False) msgs.connect('response', self.on_messages_dialog_response) msgs.connect('close', self.on_messages_dialog_close_or_delete_event) msgs.connect('delete-event', self.on_messages_dialog_close_or_delete_event) def create_helper_log_dialog(self): log = gtk.Dialog(title='Redeye: Output Log')#, parent=self.mainwin) self.helper_log_dialog = log log.set_default_size(gtk.gdk.screen_width()/4,gtk.gdk.screen_height()/3) for logattr in ('helper_log_mkisofs', 'helper_log_cdrecord'): logview = gtk.TextView() setattr(self, logattr, logview) logview.set_editable(False) logview.set_cursor_visible(False) buf = logview.get_buffer() table = buf.get_tag_table() out_tag = gtk.TextTag('stdout') out_tag.set_property('foreground', 'blue') table.add(out_tag) err_tag = gtk.TextTag('stderr') err_tag.set_property('foreground', 'black') table.add(err_tag) head_tag = gtk.TextTag('header') head_tag.set_property('scale', pango.SCALE_LARGE) head_tag.set_property('weight', pango.WEIGHT_BOLD) table.add(head_tag) scrolledwin = gtk.ScrolledWindow() scrolledwin.add(logview) label = gtk.Label(logattr.split('_')[-1] + ':') label.set_padding(5,3) label.set_alignment(0,0) log.vbox.pack_start(label, expand=False) log.vbox.pack_start(scrolledwin) log.vbox.show_all() log.RESPONSE_CLEAR = 1 log.add_button('gtk-clear', log.RESPONSE_CLEAR) log.add_button('gtk-close', gtk.RESPONSE_CLOSE) log.set_has_separator(False) log.connect('response', self.on_helper_log_response) log.connect('close', self.on_helper_log_close_or_delete_event) log.connect('delete-event', self.on_helper_log_close_or_delete_event) def helper_log(self, logview, message, tag): message = message.strip() message = message.decode('iso-8859-1', 'replace').encode('utf-8')+'\n' buf = logview.get_buffer() sob, eob = buf.get_bounds() buf.insert_with_tags_by_name(eob, message, tag) logview.scroll_to_mark(buf.get_insert(), 0) def burn_layout(self): self.info("Preparing CD...") self.button_burn.set_sensitive(False) view = self.cdview tree = view.model it = tree.get_iter_root() self.in_session = -1 self.num_sessions = tree.iter_n_children(it) self.sessions = [] if not tree.get_value(it, 0).is_root: self.abort_burn("Broken CD Root") return if tree.iter_next(it) is not None: self.abort_burn("Multiple CD Roots") return if not tree.iter_has_child(it): self.abort_burn("No sessions") return child = tree.iter_children(it) append = self.sessions.append while child: session_num = (len(self.sessions)+1, self.num_sessions) rowobj = tree.get_value(child, 0) if rowobj.is_session: try: append(rowobj.create_session(child, session_num)) except ValueError, err: self.abort_burn("Error creating %s: %s" % (str(rowobj), str(err))) return except NotImplementedError: self.abort_burn("Invalid Session %d, type=%s" % (session_num[0], rowobj.__class__.__name__)) return else: self.abort_burn("Non session in root, type=%s" % rowobj.__class__.__name__) child = tree.iter_next(child) self.info("Burning CD") self.burn_next_session() def burn_next_session(self): self.in_session += 1 ctx = 1 self.statusbar.pop(ctx) self.statusbar.push(ctx, 'Burning Session %d of %d' % (self.in_session+1, self.num_sessions)) if self.in_session == self.num_sessions: self.abort_burn() else: self.session = self.sessions[self.in_session] message = 'Session %d:' % (self.in_session+1) self.helper_log(self.helper_log_mkisofs, message, 'header') self.helper_log(self.helper_log_cdrecord, message, 'header') try: self.session.start() except ValueError, err: self.error("Error starting session %d: %s" % (self.num_sessions+1, str(err))) self.abort_burn("Error starting session %d" % (self.in_session+1)) def abort_burn(self, message=None): #ctx = 1 self.statusbar.pop(1) if self.in_session == self.num_sessions: if message: #self.statusbar.push(ctx, message) self.error(message, True) else: #self.statusbar.pop(ctx) #self.statusbar.push(ctx, 'Burn Successful') self.info('Burn Successful', True) elif self.in_session == -1: message = message or ('Error creating session %d' % (len(self.sessions)+2)) #self.statusbar.pop(ctx) #self.statusbar.push(ctx, message) self.error(message, True) else: message = message or ('Error burning session %d' % (self.in_session+1)) #self.statusbar.pop(ctx) #self.statusbar.push(ctx, message) self.error(message, True) self.button_burn.set_sensitive(True) # clean up old session info self.in_session = -1 self.num_sessions = 0 self.sessions = [] def info(self, message, popup=0): self.message(message, -1, popup) def warn(self, message, popup=0): self.message(message, 0, popup) def error(self, message, popup=0): self.message(message, 1, popup) def message(self, message, error=0, popup=0): """Adds a message to the warning list, and sets the button in the lower-left to represent the highest severity received. Additionally, if popup is non-false, pop up the message in a Dialog. Severities of error are Info(error=-1), Warning (error=0), and Error (error=1).""" button = self.messages_button if message != '': # note we have a new message by raising the button button.set_relief(gtk.RELIEF_NORMAL) # update button's icon if necessary, and show it if button.status < error: button.remove(button.get_child()) if error < 0: icon = button.icon_info elif error == 0: icon = button.icon_warn else: icon = button.icon_error button.status = error button.add(icon) # add info/warning/error to the list if error < 0: pixbuf = self.messages_view.render_icon( stock_id='gtk-dialog-info', size=gtk.ICON_SIZE_MENU) elif error == 0: pixbuf = self.messages_view.render_icon( stock_id='gtk-dialog-warning', size=gtk.ICON_SIZE_MENU) else: pixbuf = self.messages_view.render_icon( stock_id='gtk-dialog-error', size=gtk.ICON_SIZE_MENU) self.messages_model.prepend((pixbuf, message, time.time())) # dialog on true popup if popup: if error < 0: kind=gtk.MESSAGE_INFO elif error == 0: kind = gtk.MESSAGE_WARNING else: kind = gtk.MESSAGE_ERROR dialog = gtk.MessageDialog(parent=self.mainwin, type=kind, buttons=gtk.BUTTONS_OK, message_format=message) dialog.set_response_sensitive(gtk.RESPONSE_OK, True) dialog.run() # ha, can't burn again until you answer dialog.hide() # but the other way, the OK doesn't hide it dialog.destroy() del dialog else: button.remove(button.get_child()) button.status = -1 button.add(button.icon_info) button.set_relief(gtk.RELIEF_NONE) if error==None: while 1: it = self.messages_model.get_iter_first() if not it: break self.messages_model.remove(it) def set_mkisofs_info(self, message=None, fraction=None): if message: self.pbar_mkisofs.set_text(message) if fraction: self.pbar_mkisofs.set_fraction(fraction) def set_cdrecord_info(self, message=None, fraction=None, fifo=None, buffer=None): if message is not None: self.pbar_cdrecord.set_text(message) if fraction is not None: self.pbar_cdrecord.set_fraction(fraction) if fifo is not None: self.pbar_fifo.set_text("fifo %d%%" % fifo) self.pbar_fifo.set_fraction(fifo/100.0) if buffer is not None: self.pbar_buffer.set_text("buffer: %d%%" % buffer) self.pbar_buffer.set_fraction(buffer/100.0) def on_quit_activate(self, *args): assert args == args # don't use this; block pychecker warns gtk.mainquit() def on_redeye_destroy_event(self, *args): assert args == args # don't use this; block pychecker warns gtk.mainquit() def on_redeye_delete_event(self, *args): assert args == args # don't use this; block pychecker warns gtk.mainquit() if __name__ == '__main__': app = Redeye() gtk.mainloop()