#!/usr/bin/env python
 
# The contents of this file are subject to the BitTorrent Open Source License
# Version 1.1 (the License).  You may not copy or use this file, in either
# source code or executable form, except in compliance with the License.  You
# may obtain a copy of the License at http://www.bittorrent.com/license/.
#
# Software distributed under the License is distributed on an AS IS basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
# for the specific language governing rights and limitations under the
# License.
 
# Written by Uoti Urpala and Matt Chisholm
 
from __future__ import division
 
from BitTorrent.platform import install_translation
install_translation()
 
import sys
import itertools
import math
import os
import threading
import datetime
import random
import atexit
 
assert sys.version_info >= (2, 3), _("Install Python %s or greater") % '2.3'
 
from BitTorrent import BTFailure, INFO, WARNING, ERROR, CRITICAL, status_dict, app_name
 
from BitTorrent import configfile
 
from BitTorrent.defaultargs import get_defaults
from BitTorrent.IPC import ipc_interface
from BitTorrent.prefs import Preferences
from BitTorrent.platform import doc_root, btspawn, path_wrap, os_version, is_frozen_exe, get_startup_dir, create_shortcut, remove_shortcut
from BitTorrent import zurllib
 
defaults = get_defaults('bittorrent')
defaults.extend((('donated' , '', ''), # the version that the user last donated for
                 ('notified', '', ''), # the version that the user was last notified of
                 ))
 
 
ui_options = [
    'max_upload_rate'       ,
    'minport'               ,
    'maxport'               ,
    'next_torrent_time'     ,
    'next_torrent_ratio'    ,
    'last_torrent_ratio'    ,
    'seed_forever'          ,
    'seed_last_forever'     ,
    'ask_for_save'          ,
    'save_in'               ,
    'open_from'             ,
    'ip'                    ,
    'start_torrent_behavior',
    'upnp'                  ,
    ]
 
if os.name == 'nt':
    ui_options.extend( [
        'launch_on_startup'     ,
        'minimize_to_tray'      ,
        ])
 
advanced_ui_options_index = len(ui_options)
 
ui_options.extend([
    'min_uploads'     ,
    'max_uploads'     ,
    'max_initiate'    ,
    'max_incomplete'  ,
    'max_allow_in'    ,
    'max_files_open'  ,
    'forwarded_port'  ,
    'display_interval',
    'donated'         ,
    'notified'        ,
    ])
 
 
if is_frozen_exe:
    ui_options.append('progressbar_hack')
    defproghack = 0
    if os_version == 'XP':
        # turn on progress bar hack by default for Win XP 
        defproghack = 1
    defaults.extend((('progressbar_hack' , defproghack, ''),)) 
 
 
NAG_FREQUENCY = 3
PORT_RANGE = 5
 
defconfig = dict([(name, value) for (name, value, doc) in defaults])
del name, value, doc
 
def btgui_exit(ipc):
    ipc.stop()
 
class global_logger(object):
    def __init__(self, logger = None):
        self.logger = logger
    def __call__(self, severity, msg):
        if self.logger:
            self.logger(severity, msg)
        else:
            sys.stderr.write("%s: %s\n" % (status_dict[severity], msg))        
 
# if it's application global, why do we pass a reference to it everywhere?
global_log_func = global_logger()
 
if __name__ == '__main__':
    zurllib.add_unsafe_thread()
 
    try:
        config, args = configfile.parse_configuration_and_args(defaults,
                                        'bittorrent', sys.argv[1:], 0, None)
    except BTFailure, e:
        print str(e)
        sys.exit(1)
 
    config = Preferences().initWithDict(config)
    advanced_ui = config['advanced']
 
    newtorrents = args
    for opt in ('responsefile', 'url'):
        if config[opt]:
            print '"--%s"' % opt, _("deprecated, do not use")
            newtorrents.append(config[opt])
 
    ipc = ipc_interface(config, global_log_func)
 
    # this could be on the ipc object
    ipc_master = True
    try:
        ipc.create()
    except BTFailure:
        ipc_master = False
 
        try:
            ipc.send_command('no-op')
        except BTFailure:
            global_log_func(ERROR, _("Failed to communicate with another %s process "
                                     "but one seems to be running.") + 
                                   _(" Closing all %s windows may fix the problem.")
                                   % (app_name, app_name))
            sys.exit(1)
 
    # make sure we clean up the ipc when we close
    atexit.register(btgui_exit, ipc)
 
    # it's not obvious, but 'newtorrents' is carried on to the gui
    # __main__ if we're the IPC master
 
    if not ipc_master:
 
        if newtorrents:
            # Not sure if anything really useful could be done if
            # these send_command calls fail
            for name in newtorrents:
                ipc.send_command('start_torrent', name, config['save_as'])
            sys.exit(0)
 
        try:
            ipc.send_command('show_error', _("%s already running")%app_name)
        except BTFailure:
            global_log_func(ERROR, _("Failed to communicate with another %s process.") +
                                   _(" Closing all %s windows may fix the problem.")
                                   % app_name)
        sys.exit(1)
 
 
import gtk
import pango
import gobject
import webbrowser
 
assert gtk.pygtk_version >= (2, 6), _("PyGTK %s or newer required") % '2.6'
 
from BitTorrent import HELP_URL, DONATE_URL, SEARCH_URL, version, branch
 
from BitTorrent import TorrentQueue
from BitTorrent import LaunchPath
from BitTorrent import Desktop
from BitTorrent import ClientIdentifier
from BitTorrent import NewVersion
 
from BitTorrent.parseargs import makeHelp
from BitTorrent.TorrentQueue import RUNNING, RUN_QUEUED, QUEUED, KNOWN, ASKING_LOCATION
from BitTorrent.TrayIcon import TrayIcon
from BitTorrent.StatusLight import GtkStatusLight as StatusLight
from BitTorrent.GUI import * 
 
 
main_torrent_dnd_tip = _("drag to reorder")
torrent_menu_tip = _("right-click for menu")
torrent_tip_format = '%s:\n %s\n %s'
 
rate_label = ': %s'
 
speed_classes = {
    (   4,    5):_("dialup"           ),
    (   6,   14):_("DSL/cable 128k up"),
    (  15,   29):_("DSL/cable 256k up"),
    (  30,   91):_("DSL 768k up"      ),
    (  92,  137):_("T1"               ),
    ( 138,  182):_("T1/E1"            ),
    ( 183,  249):_("E1"               ),
    ( 250, 5446):_("T3"               ),
    (5447,18871):_("OC3"              ),
    }
 
def find_dir(path):
    if os.path.isdir(path):
        return path
    directory, garbage = os.path.split(path)
    while directory:
        if os.access(directory, os.F_OK) and os.access(directory, os.W_OK):
            return directory
        directory, garbage = os.path.split(directory)
        if garbage == '':
            break        
    return None
 
def smart_dir(path):
    path = find_dir(path)
    if path is None:
        path = Desktop.desktop
    return path
 
class MenuItem(gtk.MenuItem): 
    def __init__(self, label, accel_group=None, func=None):
        gtk.MenuItem.__init__(self, label)
        if func is not None:
            self.connect("activate", func)
        else:
            self.set_sensitive(False)
 
        if accel_group is not None:
            label = label.decode('utf-8')
            accel_index = label.find('_')
            if -1 < accel_index < len(label) - 1:
                accel_char = long(ord(label[accel_index+1]))
                accel_key  = gtk.gdk.unicode_to_keyval(accel_char)
                if accel_key != accel_char | 0x01000000:
                    self.add_accelerator("activate", accel_group, accel_key,
                                         gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)
        self.show()
 
 
def build_menu(menu_items, accel_group=None):
    menu = gtk.Menu()
    for label,func in menu_items:
        if label == '----':
            s = gtk.SeparatorMenuItem()
            s.show()
            menu.add(s)
        else:
            item = MenuItem(label, accel_group=accel_group, func=func)
            item.show()
            menu.add(item)
    return menu
 
 
class Validator(gtk.Entry):
    valid_chars = '1234567890'
    minimum = None
    maximum = None
    cast = int
 
    def __init__(self, option_name, config, setfunc):
        gtk.Entry.__init__(self)
        self.option_name = option_name
        self.config      = config
        self.setfunc     = setfunc
 
        self.set_text(str(config[option_name]))
 
        self.set_size_request(self.width,-1)
 
        self.connect('insert-text', self.text_inserted)
        self.connect('focus-out-event', self.focus_out)
 
    def get_value(self):
        value = None
        try:
            value = self.cast(self.get_text())
        except ValueError:
            pass
        return value
 
    def set_value(self, value):
        self.set_text(str(value))
        self.setfunc(self.option_name, value)        
 
    def focus_out(self, entry, widget):
        value = self.get_value()
 
        if value is None:
            return
 
        if (self.minimum is not None) and (value < self.minimum):
            value = self.minimum
        if (self.maximum is not None) and (value > self.maximum):
            value = self.maximum
 
        self.set_value(value)
 
    def text_inserted(self, entry, input, position, user_data):
        for i in input:
            if (self.valid_chars is not None) and (i not in self.valid_chars):
                self.emit_stop_by_name('insert-text')
                return True
        return False
 
class IPValidator(Validator):
    valid_chars = '1234567890.'
    width = 128
    cast = str
 
class PortValidator(Validator):
    width = 64
    minimum = 1024
    maximum = 65535
 
    def add_end(self, end_name):
        self.end_option_name = end_name
 
    def set_value(self, value):
        self.set_text(str(value))
        self.setfunc(self.option_name, value)
        self.setfunc(self.end_option_name, value+PORT_RANGE)
 
 
class PercentValidator(Validator):
    width = 48
    minimum = 0
 
class MinutesValidator(Validator):
    width = 48
    minimum = 1
 
class EnterUrlDialog(MessageDialog):
    flags = gtk.DIALOG_DESTROY_WITH_PARENT
    def __init__(self, parent):
        self.entry = gtk.Entry()
        self.entry.show()
        self.main = parent
        MessageDialog.__init__(self, parent.mainwindow,
                               _("Enter torrent URL"),
                               _("Enter the URL of a torrent file to open:"),
                               type=gtk.MESSAGE_QUESTION,
                               buttons=gtk.BUTTONS_OK_CANCEL,
                               yesfunc=lambda *args: parent.open_url(self.entry.get_text()),
                               default=gtk.RESPONSE_OK
                               )
        hbox = gtk.HBox()
        hbox.pack_start(self.entry, padding=SPACING)
        hbox.show()
        self.entry.set_activates_default(True)
        self.entry.set_flags(gtk.CAN_FOCUS)
        self.vbox.pack_start(hbox)
        self.entry.grab_focus()
 
    def close(self, *args):
        self.destroy()
 
    def destroy(self):
        MessageDialog.destroy(self)
        self.main.window_closed('enterurl')
 
 
class RateSliderBox(gtk.VBox):
    base = 10
    multiplier = 4
    max_exponent = 3.3
 
    def __init__(self, config, torrentqueue):
        gtk.VBox.__init__(self, homogeneous=False)
        self.config = config
        self.torrentqueue = torrentqueue
 
        if self.config['max_upload_rate'] < self.slider_to_rate(0):
            self.config['max_upload_rate'] = self.slider_to_rate(0)
 
        self.speed_classes = {
            (   4,    5):_("dialup"           ),
            (   6,   14):_("DSL/cable 128k up"),
            (  15,   29):_("DSL/cable 256k up"),
            (  30,   91):_("DSL 768k up"      ),
            (  92,  137):_("T1"               ),
            ( 138,  182):_("T1/E1"            ),
            ( 183,  249):_("E1"               ),
            ( 250, 5446):_("T3"               ),
            (5447,18871):_("OC3"              ),
            }
 
        biggest_size = 0
        for v in self.speed_classes.values():
            width = gtk.Label(v).size_request()[0]
            if width > biggest_size:
                biggest_size = width
 
        self.rate_slider_label_box = gtk.HBox(spacing=SPACING,
                                              homogeneous=True)
 
        self.rate_slider_label = gtk.Label(_("Maximum upload rate:"))
        self.rate_slider_label.set_ellipsize(pango.ELLIPSIZE_START)
        self.rate_slider_label.set_alignment(1, 0.5)
        self.rate_slider_label_box.pack_start(self.rate_slider_label,
                                              expand=True, fill=True)
 
        self.rate_slider_value = gtk.Label(
            self.value_to_label(self.config['max_upload_rate']))
        self.rate_slider_value.set_alignment(0, 0.5)
        self.rate_slider_value.set_size_request(biggest_size, -1)
 
        self.rate_slider_label_box.pack_start(self.rate_slider_value,
                                              expand=True, fill=True)
 
        self.rate_slider_adj = gtk.Adjustment(
            self.rate_to_slider(self.config['max_upload_rate']), 0,
            self.max_exponent, 0.01, 0.1)
 
        self.rate_slider = gtk.HScale(self.rate_slider_adj)
        self.rate_slider.set_draw_value(False)
        self.rate_slider_adj.connect('value_changed', self.set_max_upload_rate)
 
        self.pack_start(self.rate_slider       , expand=False, fill=False)
        self.pack_start(self.rate_slider_label_box , expand=False, fill=False)
 
        if False: # this shows the legend for the slider
            self.rate_slider_legend = gtk.HBox(homogeneous=True)
            for i in range(int(self.max_exponent+1)):
                label = gtk.Label(str(self.slider_to_rate(i)))
                alabel = halign(label, i/self.max_exponent)
                self.rate_slider_legend.pack_start(alabel,
                                                   expand=True, fill=True)
            self.pack_start(self.rate_slider_legend, expand=False, fill=False)
 
 
    def start(self):
        self.set_max_upload_rate(self.rate_slider_adj)
 
    def rate_to_slider(self, value):
        return math.log(value/self.multiplier, self.base)
 
    def slider_to_rate(self, value):
        return int(round(self.base**value * self.multiplier))
 
    def value_to_label(self, value):
        conn_type = ''
        for key, conn in self.speed_classes.items():
            min_v, max_v = key
            if min_v <= value <= max_v:
                conn_type = ' (%s)'%conn
                break
        label = str(Rate(value*1024)) + conn_type
        return label
 
    def set_max_upload_rate(self, adj):
        option = 'max_upload_rate'
        value = self.slider_to_rate(adj.get_value())
        self.config[option] = value
        self.torrentqueue.set_config(option, value)
        self.rate_slider_value.set_text(self.value_to_label(int(value)))
 
 
class StopStartButton(gtk.Button):
    stop_tip  = _("Temporarily stop all running torrents")
    start_tip = _("Resume downloading")
 
    def __init__(self, main):
        gtk.Button.__init__(self)
        self.main = main
        self.connect('clicked', self.toggle)
 
        self.stop_image = gtk.Image()
        self.stop_image.set_from_stock('bt-pause', gtk.ICON_SIZE_BUTTON)
        self.stop_image.show()
 
        self.start_image = gtk.Image()
        self.start_image.set_from_stock('bt-play', gtk.ICON_SIZE_BUTTON)
        self.start_image.show()
 
    def toggle(self, widget):
        self.set_paused(not self.main.config['pause'])
 
    def set_paused(self, paused):
        image = self.get_child()
        if paused:
            if image == self.stop_image:
                self.remove(self.stop_image)
            if image != self.start_image:
                self.add(self.start_image)
            self.main.tooltips.set_tip(self, self.start_tip)
            self.main.stop_queue()
        else:
            if image == self.start_image:
                self.remove(self.start_image)
            if image != self.stop_image:
                self.add(self.stop_image)
            self.main.tooltips.set_tip(self, self.stop_tip )
            self.main.restart_queue()
 
 
class VersionWindow(Window):
    def __init__(self, main, newversion, download_url):
        Window.__init__(self)
        self.set_title(_("New %s version available")%app_name)
        self.set_border_width(SPACING)
        self.set_resizable(False)
        self.main = main
        self.newversion = newversion
        self.download_url = download_url
        self.connect('destroy', lambda w: self.main.window_closed('version'))
        self.vbox = gtk.VBox(spacing=SPACING)
        self.hbox = gtk.HBox(spacing=SPACING)
        self.image = gtk.Image()
        self.image.set_from_stock(gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_DIALOG)
        self.hbox.pack_start(self.image)
 
        self.label = gtk.Label()
        self.label.set_markup(
            (_("A newer version of %s is available.\n") % app_name) +
            (_("You are using %s, and the new version is %s.\n") % (version, newversion)) +
            (_("You can always get the latest version from \n%s") % self.download_url)
            ) 
        self.label.set_selectable(True)
        self.hbox.pack_start(self.label)
        self.vbox.pack_start(self.hbox)
        self.bbox = gtk.HBox(spacing=SPACING)
 
        self.closebutton = gtk.Button(_("Download _later"))
        self.closebutton.connect('clicked', self.close)
 
        self.newversionbutton = gtk.Button(_("Download _now"))
        self.newversionbutton.connect('clicked', self.get_newversion)
 
        self.bbox.pack_end(self.newversionbutton, expand=False, fill=False)
        self.bbox.pack_end(self.closebutton     , expand=False, fill=False)
 
        self.checkbox = gtk.CheckButton(_("_Remind me later"))
        self.checkbox.set_active(True)
        self.checkbox.connect('toggled', self.remind_toggle)
 
        self.bbox.pack_start(self.checkbox, expand=False, fill=False)
 
        self.vbox.pack_start(self.bbox)
 
        self.add(self.vbox)
        self.show_all()
 
    def remind_toggle(self, widget):
        v = self.checkbox.get_active()
        notified = ''
        if v:
            notified = ''
        else:
            notified = self.newversion
        self.main.set_config('notified', str(notified))
 
    def close(self, widget):
        self.destroy()
 
    def get_newversion(self, widget):
        if self.main.updater.can_install():
            if self.main.updater.torrentfile is None:
                self.main.visit_url(self.download_url)
            else:
                self.main.start_auto_update()
        else:
            self.main.visit_url(self.download_url)
        self.destroy()
 
 
class AboutWindow(object):
 
    def __init__(self, main, donatefunc):
        self.win = Window()
        self.win.set_title(_("About %s")%app_name)
        self.win.set_size_request(300,400)
        self.win.set_border_width(SPACING)
        self.win.set_resizable(False)
        self.win.connect('destroy', lambda w: main.window_closed('about'))
        self.scroll = gtk.ScrolledWindow()
        self.scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
        self.scroll.set_shadow_type(gtk.SHADOW_IN)
 
        self.outervbox = gtk.VBox()
 
        self.outervbox.pack_start(get_logo(96), expand=False, fill=False)
 
        version_str = version
        if int(version_str[2]) % 2:
            version_str = version_str + ' ' + _("Beta")
 
        self.outervbox.pack_start(gtk.Label(_("Version %s")%version_str), expand=False, fill=False)
 
        if branch is not None:
            blabel = gtk.Label('cdv client dir: %s' % branch)
            self.outervbox.pack_start(blabel, expand=False, fill=False)
 
        self.vbox = gtk.VBox()
        self.vbox.set_size_request(250, -1)
 
        for i, fn in enumerate(('credits', 'credits-l10n')):
            if i != 0:
                self.vbox.pack_start(gtk.HSeparator(), padding=SPACING,
                                     expand=False, fill=False)
            filename = os.path.join(doc_root, fn+'.txt')
            l = ''
            if not os.access(filename, os.F_OK|os.R_OK):
                l = _("Couldn't open %s") % filename
            else:
                credits_f = file(filename)
                l = credits_f.read()
                credits_f.close()
            if os.name == 'nt':
                # gtk ignores blank lines on win98
                l = l.replace('\n\n', '\n\t\n')
            label = gtk.Label(l.strip())
            label.set_line_wrap(True)
            label.set_selectable(True)
            label.set_justify(gtk.JUSTIFY_CENTER)
            label.set_size_request(250,-1)
            self.vbox.pack_start(label, expand=False, fill=False)
 
        self.scroll.add_with_viewport(self.vbox)
 
        self.outervbox.pack_start(self.scroll, padding=SPACING)
 
        self.donatebutton = gtk.Button(_("Donate"))
        self.donatebutton.connect('clicked', donatefunc)
        self.donatebuttonbox = gtk.HButtonBox()
        self.donatebuttonbox.pack_start(self.donatebutton,
                                        expand=False, fill=False)
        self.outervbox.pack_end(self.donatebuttonbox, expand=False, fill=False)
 
        self.win.add(self.outervbox)
 
        self.win.show_all()
 
    def close(self, widget):
        self.win.destroy()    
 
 
class LogWindow(object):
    def __init__(self, main, logbuffer, config):
        self.config = config
        self.main = main
        self.win = Window()
        self.win.set_title(_("%s Activity Log")%app_name)
        self.win.set_default_size(600, 200)
        self.win.set_border_width(SPACING)
 
        self.buffer = logbuffer
        self.text = gtk.TextView(self.buffer)
        self.text.set_editable(False)
        self.text.set_cursor_visible(False)
        self.text.set_wrap_mode(gtk.WRAP_WORD)
 
        self.scroll = gtk.ScrolledWindow()
        self.scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
        self.scroll.set_shadow_type(gtk.SHADOW_IN)
        self.scroll.add(self.text)
 
        self.vbox = gtk.VBox(spacing=SPACING)
        self.vbox.pack_start(self.scroll)
 
        self.buttonbox = gtk.HButtonBox()
        self.buttonbox.set_spacing(SPACING)
 
        self.closebutton = gtk.Button(stock='gtk-close')
        self.closebutton.connect('clicked', self.close)
 
        self.savebutton = gtk.Button(stock='gtk-save')
        self.savebutton.connect('clicked', self.save_log_file_selection)
 
        self.clearbutton = gtk.Button(stock='gtk-clear')
        self.clearbutton.connect('clicked', self.clear_log)
 
        self.buttonbox.pack_start(self.savebutton)
        self.buttonbox.pack_start(self.closebutton)
 
        self.hbox2 = gtk.HBox(homogeneous=False)
 
        self.hbox2.pack_end(self.buttonbox, expand=False, fill=False)
 
        bb = gtk.HButtonBox()
        bb.pack_start(self.clearbutton)
        self.hbox2.pack_start(bb, expand=False, fill=True)
 
        self.vbox.pack_end(self.hbox2, expand=False, fill=True)
 
        self.win.add(self.vbox)        
        self.win.connect("destroy", lambda w: self.main.window_closed('log'))
        self.scroll_to_end()
        self.win.show_all()
 
    def scroll_to_end(self):
        mark = self.buffer.create_mark(None, self.buffer.get_end_iter())
        self.text.scroll_mark_onscreen(mark)
 
    def save_log_file_selection(self, *args):
        name = 'bittorrent.log'
        path = smart_dir(self.config['save_in'])
        fullname = os.path.join(path, name)
        self.main.open_window('savefile',
                              title=_("Save log in:"),
                              fullname=fullname,
                              got_location_func=self.save_log,
                              no_location_func=lambda: self.main.window_closed('savefile'))
 
 
    def save_log(self, saveas):
        self.main.window_closed('savefile')
        f = file(saveas, 'w')
        f.write(self.buffer.get_text(self.buffer.get_start_iter(),
                                     self.buffer.get_end_iter()))
        save_message = self.buffer.log_text(_("log saved"), None)
        f.write(save_message)
        f.close()
 
    def clear_log(self, *args):
        self.buffer.clear_log()
 
    def close(self, widget):
        self.win.destroy()
 
 
class LogBuffer(gtk.TextBuffer):
 
    def __init__(self):
        gtk.TextBuffer.__init__(self)
 
        tt = self.get_tag_table()
 
        size_tag = gtk.TextTag('small')
        size_tag.set_property('size-points', 10)
        tt.add(size_tag)
 
        info_tag = gtk.TextTag('info')
        info_tag.set_property('foreground', '#00a040')
        tt.add(info_tag)
 
        warning_tag = gtk.TextTag('warning')
        warning_tag.set_property('foreground', '#a09000')
        tt.add(warning_tag)
 
        error_tag = gtk.TextTag('error')
        error_tag.set_property('foreground', '#b00000')
        tt.add(error_tag)
 
        critical_tag = gtk.TextTag('critical')
        critical_tag.set_property('foreground', '#b00000')
        critical_tag.set_property('weight', pango.WEIGHT_BOLD)
        tt.add(critical_tag)
 
 
    def log_text(self, text, severity=CRITICAL):
        now_str = datetime.datetime.strftime(datetime.datetime.now(),
                                             '[%Y-%m-%d %H:%M:%S] ')
        self.insert_with_tags_by_name(self.get_end_iter(), now_str, 'small')
        if severity is not None:
            self.insert_with_tags_by_name(self.get_end_iter(), '%s\n'%text,
                                          'small', status_dict[severity])
        else:
            self.insert_with_tags_by_name(self.get_end_iter(),
                                          ' -- %s -- \n'%text, 'small')
 
        return now_str+text+'\n'
 
    def clear_log(self):
        self.set_text('')
        self.log_text(_("log cleared"), None)
 
 
class CheckButton(gtk.CheckButton): 
    def __init__(self, label, main, option_name, initial_value,
                 extra_callback=None):
        gtk.CheckButton.__init__(self, label)
        self.main = main
        self.option_name = option_name
        self.option_type = type(initial_value)
        self.set_active(bool(initial_value))
        self.extra_callback = extra_callback
        self.connect('toggled', self.callback)
 
    def callback(self, *args):
        self.main.config[self.option_name] = \
            self.option_type(not self.main.config[self.option_name])
        self.main.setfunc(self.option_name, self.main.config[self.option_name])
        if self.extra_callback is not None:
            self.extra_callback()
 
 
class SettingsWindow(object):
 
    def __init__(self, main, config, setfunc):
        self.main = main
        self.setfunc = setfunc
        self.config = config
        self.win = Window()
        self.win.connect("destroy", lambda w: main.window_closed('settings'))
        self.win.set_title(_("%s Settings")%app_name)
        self.win.set_border_width(SPACING)
 
        self.notebook = gtk.Notebook()
 
        self.vbox = gtk.VBox(spacing=SPACING)
        self.vbox.pack_start(self.notebook, expand=False, fill=False)
 
        # General tab
        if os.name == 'nt':
            self.cb_box = gtk.VBox(spacing=SPACING)
            self.cb_box.set_border_width(SPACING)
            self.notebook.append_page(self.cb_box, gtk.Label(_("General")))
 
            self.startup_checkbutton = CheckButton(
                _("Launch BitTorrent when Windows starts"), self,
                'launch_on_startup', self.config['launch_on_startup'])
            self.cb_box.pack_start(self.startup_checkbutton, expand=False, fill=False)
            self.startup_checkbutton.connect('toggled', self.launch_on_startup)
 
            self.minimize_checkbutton = CheckButton(
                _("Minimize to system tray"), self,
                'minimize_to_tray', self.config['minimize_to_tray'])
            self.cb_box.pack_start(self.minimize_checkbutton, expand=False, fill=False)
 
            # allow the user to set the progress bar text to all black
            self.progressbar_hack = CheckButton(
                _("Progress bar text is always black\n(requires restart)"),
                self, 'progressbar_hack', self.config['progressbar_hack'])
 
            self.cb_box.pack_start(self.progressbar_hack, expand=False, fill=False)
            # end General tab
 
        # Saving tab
        self.saving_box = gtk.VBox(spacing=SPACING)
        self.saving_box.set_border_width(SPACING)
        self.notebook.append_page(self.saving_box, gtk.Label(_("Saving")))
 
        self.dl_frame = gtk.Frame(_("Save new downloads in:"))
        self.saving_box.pack_start(self.dl_frame, expand=False, fill=False)
 
        self.dl_box = gtk.VBox(spacing=SPACING)
        self.dl_box.set_border_width(SPACING)
        self.dl_frame.add(self.dl_box)
        self.save_in_box = gtk.HBox(spacing=SPACING)
 
        self.dl_save_in = gtk.Entry()
        self.dl_save_in.set_editable(False)
        self.set_save_in(self.config['save_in'])
        self.save_in_box.pack_start(self.dl_save_in, expand=True, fill=True)
 
        self.dl_save_in_button = gtk.Button(_("Change..."))
        self.dl_save_in_button.connect('clicked', self.get_save_in)
        self.save_in_box.pack_start(self.dl_save_in_button, expand=False, fill=False)
        self.dl_box.pack_start(self.save_in_box, expand=False, fill=False)
 
        self.dl_ask_checkbutton = CheckButton(
            _("Ask where to save each new download"), self,
            'ask_for_save', self.config['ask_for_save'])
 
        self.dl_box.pack_start(self.dl_ask_checkbutton, expand=False, fill=False)
        # end Saving tab
 
        # Downloading tab
        self.downloading_box = gtk.VBox(spacing=SPACING)
        self.downloading_box.set_border_width(SPACING)
        self.notebook.append_page(self.downloading_box, gtk.Label(_("Downloading")))
 
        self.dnd_frame = gtk.Frame(_("When starting a new torrent:"))
        self.dnd_box = gtk.VBox(spacing=SPACING, homogeneous=True)
        self.dnd_box.set_border_width(SPACING)
 
        self.dnd_states = ['replace','add','ask']
        self.dnd_original_state = self.config['start_torrent_behavior']
 
        self.always_replace_radio = gtk.RadioButton(
            group=None,
            label=_("_Stop another running torrent to make room"))
        self.dnd_box.pack_start(self.always_replace_radio)
        self.always_replace_radio.state_name = self.dnd_states[0]
 
        self.always_add_radio = gtk.RadioButton(
            group=self.always_replace_radio,
            label=_("_Don't stop other running torrents"))
        self.dnd_box.pack_start(self.always_add_radio)
        self.always_add_radio.state_name = self.dnd_states[1]
 
        self.always_ask_radio = gtk.RadioButton(
            group=self.always_replace_radio,
            label=_("_Ask each time")
            )
        self.dnd_box.pack_start(self.always_ask_radio)
        self.always_ask_radio.state_name = self.dnd_states[2]
 
        self.dnd_group = self.always_replace_radio.get_group()
        for r in self.dnd_group:
            r.connect('toggled', self.start_torrent_behavior_changed)
 
        self.set_start_torrent_behavior(self.config['start_torrent_behavior'])
 
        self.dnd_frame.add(self.dnd_box)
        self.downloading_box.pack_start(self.dnd_frame, expand=False, fill=False)
 
        # Seeding tab
        self.seeding_box = gtk.VBox(spacing=SPACING)
        self.seeding_box.set_border_width(SPACING)
        self.notebook.append_page(self.seeding_box, gtk.Label(_("Seeding")))
 
        def colon_split(framestr):
            COLONS = (':', u'\uff1a')
            for colon in COLONS:
                if colon in framestr:
                    return framestr.split(colon)
            return '', framestr
 
        nt_framestr = _("Seed completed torrents: until share ratio reaches [_] percent, or for [_] minutes, whichever comes first.")
        nt_title, nt_rem = colon_split(nt_framestr)
        nt_msg1, nt_msg2, nt_msg4 = nt_rem.split('[_]')
        nt_msg3 = ''
        if ',' in nt_msg2:
            nt_msg2, nt_msg3 = nt_msg2.split(',')
            nt_msg2 += ','
 
        self.next_torrent_frame = gtk.Frame(nt_title+':')
        self.next_torrent_box   = gtk.VBox(spacing=SPACING, homogeneous=True)
        self.next_torrent_box.set_border_width(SPACING) 
 
        self.next_torrent_frame.add(self.next_torrent_box)
 
 
        self.next_torrent_ratio_box = gtk.HBox()
        self.next_torrent_ratio_box.pack_start(gtk.Label(nt_msg1),
                                               fill=False, expand=False)
        self.next_torrent_ratio_field = PercentValidator('next_torrent_ratio',
                                                         self.config, self.setfunc)
        self.next_torrent_ratio_box.pack_start(self.next_torrent_ratio_field,
                                               fill=False, expand=False)
        self.next_torrent_ratio_box.pack_start(gtk.Label(nt_msg2),
                                               fill=False, expand=False)
        self.next_torrent_box.pack_start(self.next_torrent_ratio_box)
 
 
        self.next_torrent_time_box = gtk.HBox()
        self.next_torrent_time_box.pack_start(gtk.Label(nt_msg3),
                                              fill=False, expand=False)
        self.next_torrent_time_field = MinutesValidator('next_torrent_time',
                                                        self.config, self.setfunc)
        self.next_torrent_time_box.pack_start(self.next_torrent_time_field,
                                              fill=False, expand=False)
        self.next_torrent_time_box.pack_start(gtk.Label(nt_msg4),
                                              fill=False, expand=False)
        self.next_torrent_box.pack_start(self.next_torrent_time_box)
 
        def seed_forever_extra():
            for field in (self.next_torrent_ratio_field,
                          self.next_torrent_time_field):
                field.set_sensitive(not self.config['seed_forever'])
 
        seed_forever_extra()
        self.seed_forever = CheckButton( _("Seed indefinitely"), self,
                                         'seed_forever',
                                         self.config['seed_forever'],
                                         seed_forever_extra)
        self.next_torrent_box.pack_start(self.seed_forever)
        # end next torrent seed behavior
 
        # begin last torrent seed behavior
        lt_framestr = _("Seed last completed torrent: until share ratio reaches [_] percent.")
        lt_title, lt_rem = colon_split(lt_framestr)
        lt_msg1, lt_msg2 = lt_rem.split('[_]')
 
        self.seeding_box.pack_start(self.next_torrent_frame, expand=False, fill=False)
 
        self.last_torrent_frame = gtk.Frame(lt_title+':')
        self.last_torrent_vbox = gtk.VBox(spacing=SPACING)
        self.last_torrent_vbox.set_border_width(SPACING)
        self.last_torrent_box = gtk.HBox()
        self.last_torrent_box.pack_start(gtk.Label(lt_msg1),
                                         expand=False, fill=False)
        self.last_torrent_ratio_field = PercentValidator('last_torrent_ratio',
                                                         self.config, self.setfunc)
        self.last_torrent_box.pack_start(self.last_torrent_ratio_field,
                                         fill=False, expand=False)
        self.last_torrent_box.pack_start(gtk.Label(lt_msg2),
                                         fill=False, expand=False)
        self.last_torrent_vbox.pack_start(self.last_torrent_box)
 
        def seed_last_forever_extra():
            self.last_torrent_ratio_field.set_sensitive(
                not self.config['seed_last_forever'])
 
        seed_last_forever_extra()
 
        self.seed_last_forever = CheckButton(_("Seed indefinitely"), self,
                                             'seed_last_forever',
                                             self.config['seed_last_forever'],
                                             seed_last_forever_extra)
        self.last_torrent_vbox.pack_start(self.seed_last_forever)
 
        self.last_torrent_frame.add(self.last_torrent_vbox)
        self.seeding_box.pack_start(self.last_torrent_frame, expand=False, fill=False)
 
        # Network tab
        self.network_box = gtk.VBox(spacing=SPACING)
        self.network_box.set_border_width(SPACING)
        self.notebook.append_page(self.network_box, gtk.Label(_("Network")))
 
        self.port_range_frame = gtk.Frame(_("Look for available port:"))        
        self.port_range_box = gtk.VBox(spacing=SPACING)
        self.port_range_box.set_border_width(SPACING)
 
        self.port_range = gtk.HBox()
        self.port_range.pack_start(gtk.Label(_("starting at port: ")),
                                   expand=False, fill=False)
        self.minport_field = PortValidator('minport', self.config, self.setfunc)
        self.minport_field.add_end('maxport')
        self.port_range.pack_start(self.minport_field, expand=False, fill=False)
        self.minport_field.settingswindow = self
        self.port_range.pack_start(gtk.Label(' (1024-65535)'),
                                   expand=False, fill=False)
        self.port_range_box.pack_start(self.port_range,
                                       expand=False, fill=False)
 
        self.upnp = CheckButton(_("Enable automatic port mapping")+' (_UPnP)',
                                self, 'upnp', self.config['upnp'], None)
        self.port_range_box.pack_start(self.upnp,
                                   expand=False, fill=False)
 
        self.port_range_frame.add(self.port_range_box)
        self.network_box.pack_start(self.port_range_frame, expand=False, fill=False)
 
        self.ip_frame = gtk.Frame(_("IP to report to the tracker:"))
        self.ip_box = gtk.VBox()
        self.ip_box.set_border_width(SPACING)
        self.ip_field = IPValidator('ip', self.config, self.setfunc)
        self.ip_box.pack_start(self.ip_field, expand=False, fill=False)
        label = gtk.Label(_("(Has no effect unless you are on the\nsame local network as the tracker)"))
        label.set_line_wrap(True)
        self.ip_box.pack_start(lalign(label), expand=False, fill=False)
        self.ip_frame.add(self.ip_box)
        self.network_box.pack_start(self.ip_frame, expand=False, fill=False)
 
        # end Network tab        
 
        # Language tab
        self.languagechooser = LanguageChooser()
        self.notebook.append_page(self.languagechooser, gtk.Label("Language"))
        # end Language tab
 
        # Advanced tab
        if advanced_ui:
            self.advanced_box = gtk.VBox(spacing=SPACING)
            self.advanced_box.set_border_width(SPACING)
            hint = gtk.Label(_("WARNING: Changing these settings can\nprevent %s from functioning correctly.")%app_name)
            self.advanced_box.pack_start(lalign(hint), expand=False, fill=False)
            self.store = gtk.ListStore(*[gobject.TYPE_STRING] * 2)
            for option in ui_options[advanced_ui_options_index:]:
                self.store.append((option, str(self.config[option])))
 
            self.treeview = gtk.TreeView(self.store)
            r = gtk.CellRendererText()
            column = gtk.TreeViewColumn(_("Option"), r, text=0)
            self.treeview.append_column(column)
            r = gtk.CellRendererText()
            r.set_property('editable', True)
            r.connect('edited', self.store_value_edited)
            column = gtk.TreeViewColumn(_("Value"), r, text=1)
            self.treeview.append_column(column)
            self.advanced_frame = gtk.Frame()
            self.advanced_frame.set_shadow_type(gtk.SHADOW_IN)
            self.advanced_frame.add(self.treeview)
 
            self.advanced_box.pack_start(self.advanced_frame, expand=False, fill=False)
            self.notebook.append_page(self.advanced_box, gtk.Label(_("Advanced")))
 
 
        self.win.add(self.vbox)
        self.win.show_all()
 
 
    def get_save_in(self, widget=None):
        self.file_selection = self.main.open_window('choosefolder',
                                                    title=_("Choose default download directory"),
                                                    fullname=self.config['save_in'],
                                                    got_location_func=self.set_save_in,
                                                    no_location_func=lambda: self.main.window_closed('choosefolder'))
 
    def set_save_in(self, save_location):
        self.main.window_closed('choosefolder')
        if os.path.isdir(save_location):
            if save_location[-1] != os.sep:
                save_location += os.sep
            self.config['save_in'] = save_location
            save_in = path_wrap(self.config['save_in'])
            self.dl_save_in.set_text(save_in)
            self.setfunc('save_in', self.config['save_in'])
 
    def launch_on_startup(self, *args):
        dst = os.path.join(get_startup_dir(), app_name)
        if self.config['launch_on_startup']:
            src = os.path.abspath(sys.argv[0])
            create_shortcut(src, dst, "--start_minimized")
        else:
            try:
                remove_shortcut(dst)
            except Exception, e:
                self.main.global_error(WARNING, _("Failed to remove shortcut: %s") % str(e))
 
    def set_start_torrent_behavior(self, state_name):
        if state_name in self.dnd_states:
            for r in self.dnd_group:
                if r.state_name == state_name:
                    r.set_active(True)
                else:
                    r.set_active(False)
        else:
            self.always_replace_radio.set_active(True)        
 
    def start_torrent_behavior_changed(self, radiobutton):
        if radiobutton.get_active():
            self.setfunc('start_torrent_behavior', radiobutton.state_name)
 
    def store_value_edited(self, cell, row, new_text):
        it = self.store.get_iter_from_string(row)
        option = ui_options[int(row)+advanced_ui_options_index]
        t = type(defconfig[option])
        try:
            if t is type(None) or t is str:
                value = new_text
            elif t is int or t is long:
                value = int(new_text)
            elif t is float:
                value = float(new_text)
            elif t is bool:
                value = value == 'True'
            else:
                raise TypeError, str(t)
        except ValueError:
            return
        self.setfunc(option, value)
        self.store.set(it, 1, str(value))
 
    def close(self, widget):
        self.win.destroy()
 
 
class FileListWindow(object):
 
    SET_PRIORITIES = False
 
    def __init__(self, metainfo, closefunc):
        self.metainfo = metainfo
        self.setfunc = None
        self.allocfunc = None
        self.win = Window()
        self.win.set_title(_('Files in "%s"') % self.metainfo.name)
        self.win.connect("destroy", closefunc)
        self.tooltips = gtk.Tooltips()
 
        self.filepath_to_iter = {}
 
        self.box1 = gtk.VBox()
 
        size_request = (0,0)
        if self.SET_PRIORITIES:
            self.toolbar = gtk.Toolbar()
            for label, tip, stockicon, method, arg in (
                (_("Never" ), _("Never download"   ), gtk.STOCK_DELETE, self.dosomething, -1,),
                (_("Normal"), _("Download normally"), gtk.STOCK_NEW   , self.dosomething,  0,),
                (_("First" ), _("Download first"   ),'bt-finished'    , self.dosomething, +1,),):
                self.make_tool_item(label, tip, stockicon, method, arg)
            size_request = (-1,54)
            self.box1.pack_start(self.toolbar, False)
 
        self.sw = gtk.ScrolledWindow()
        self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.box1.pack_start(self.sw)
        self.win.add(self.box1)
 
        columns = [_("Filename"),_("Length"),_('%')]
        pre_size_list = ['M'*30, '6666 MB', '100.0', 'Download','black']
        if self.SET_PRIORITIES:
            columns.append(_("Download"))
        num_columns = len(pre_size_list)
 
        self.store = gtk.TreeStore(*[gobject.TYPE_STRING] * num_columns)
        self.store.append(None, pre_size_list)
        self.treeview = gtk.TreeView(self.store)
        self.treeview.set_enable_search(True)
        self.treeview.set_search_column(0)
        cs = []
        for i, name in enumerate(columns):
            r = gtk.CellRendererText()
            r.set_property('xalign', (0, 1, 1, 1)[i])
            if i == 0:
                column = gtk.TreeViewColumn(name, r, text = i, foreground = len(pre_size_list)-1)
            else:
                column = gtk.TreeViewColumn(name, r, text = i)
            column.set_resizable(True)
            self.treeview.append_column(column)
            cs.append(column)
 
        self.sw.add(self.treeview)
        self.treeview.set_headers_visible(False)
        self.treeview.columns_autosize()
        self.box1.show_all()
        self.treeview.realize()
 
        for column in cs:
            column.set_fixed_width(max(5,column.get_width()))
            column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
        self.treeview.set_headers_visible(True)
        self.store.clear()
 
        if self.SET_PRIORITIES:
            self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
        else:
            self.treeview.get_selection().set_mode(gtk.SELECTION_NONE)
 
        self.piecelen = self.metainfo.piece_length
        self.lengths = self.metainfo.sizes
        self.initialize_file_priorities()#[0,0])
        for name, size, priority in itertools.izip(self.metainfo.orig_files,
                                        self.metainfo.sizes, self.priorities):
            parent_name, local_name = os.path.split(name)
            parent_iter = self.recursive_add(parent_name)
 
            row = [local_name, Size(size), '?','', 'black']
            it = self.store.append(parent_iter, row)
            self.filepath_to_iter[name] = it
 
        self.treeview.expand_all()
        tvsr = self.treeview.size_request()
        vertical_padding = 18 
        size_request = [max(size_request[0],tvsr[0]),
                        (size_request[1] + tvsr[1] ) + vertical_padding]
        maximum_height = 300
        if size_request[1] > maximum_height - SCROLLBAR_WIDTH:
            size_request[1] = maximum_height
            size_request[0] = size_request[0] + SCROLLBAR_WIDTH
        self.win.set_default_size(*size_request)
 
        self.win.show_all()
 
    def recursive_add(self, fullpath):
        if fullpath == '':
            return None
        elif self.filepath_to_iter.has_key(fullpath):
            return self.filepath_to_iter[fullpath]
        else:
            parent_path, local_path = os.path.split(fullpath)
            parent_iter = self.recursive_add(parent_path)
            it = self.store.append(parent_iter,
                                   (local_path,) +
                                   ('',) * (self.store.get_n_columns()-2) +
                                   ('black',))
            self.filepath_to_iter[fullpath] = it
            return it
 
    def make_tool_item(self, label, tip, stockicon, method, arg): 
        icon = gtk.Image()
        icon.set_from_stock(stockicon, gtk.ICON_SIZE_SMALL_TOOLBAR)
        item = gtk.ToolButton(icon_widget=icon, label=label)
        item.set_homogeneous(True)
        item.set_tooltip(self.tooltips, tip)
        if arg is not None:
            item.connect('clicked', method, arg)
        else:
            item.connect('clicked', method)
        self.toolbar.insert(item, 0)
 
    def initialize_file_priorities(self):
        self.priorities = []
        for length in self.lengths:
            self.priorities.append(0)
 
## Uoti wrote these methods. I have no idea what this code is supposed to do.
##        --matt
##    def set_priorities(self, widget):
##        r = []
##        piece = 0
##        pos = 0
##        curprio = prevprio = 1000
##        for priority, length in itertools.izip(self.priorities, self.lengths):
##            pos += length
##            curprio = min(priority, curprio)
##            while pos >= (piece + 1) * self.piecelen:
##                if curprio != prevprio:
##                    r.extend((piece, curprio))
##                prevprio = curprio
##                if curprio == priority:
##                    piece = pos // self.piecelen
##                else:
##                    piece += 1
##                if pos == piece * self.piecelen:
##                    curprio = 1000
##                else:
##                    curprio = priority
##        if curprio != prevprio:
##            r.extend((piece, curprio))
##        self.setfunc(r)
##        it = self.store.get_iter_first()
##        for i in xrange(len(self.priorities)):
##            self.store.set_value(it, 5, "black")
##            it = self.store.iter_next(it)
##        self.origpriorities = list(self.priorities)
##
##    def initialize_file_priorities(self, piecepriorities):
##        self.priorities = []
##        piecepriorities = piecepriorities + [999999999]
##        it = iter(piecepriorities)
##        assert it.next() == 0
##        pos = piece = curprio = 0
##        for length in self.lengths:
##            pos += length
##            priority = curprio
##            while pos >= piece * self.piecelen:
##                curprio = it.next()
##                if pos > piece * self.piecelen:
##                    priority = max(priority, curprio)
##                piece = it.next()
##            self.priorities.append(priority)
##        self.origpriorities = list(self.priorities)
 
    def dosomething(self, widget, dowhat):
        self.treeview.get_selection().selected_foreach(self.adjustfile, dowhat)
 
    def adjustfile(self, treemodel, path, it, dowhat):
        length = treemodel.get(it, 1)[0]
        if length == '':
            child = treemodel.iter_children(it)
            while True:
                if child is None:
                    return
                elif not treemodel.is_ancestor(it, child):
                    return
                else:
                    self.adjustfile(treemodel, path, child, dowhat)
                child = treemodel.iter_next(child)
 
        else:
            # BUG: need to set file priorities in backend here
            if dowhat == -1:
                text, color = _("never"), 'darkgrey'
            elif dowhat == 1:
                text, color = _("first"), 'darkgreen'
            else:
                text, color = '', 'black'
            treemodel.set_value(it, 3, text )
            treemodel.set_value(it, 4, color)
 
    def update(self, left, allocated):
        for name, left, total, alloc in itertools.izip(
            self.metainfo.orig_files, left, self.lengths, allocated):
            it = self.filepath_to_iter[name]
            if total == 0:
                p = 1
            else:
                p = (total - left) / total
            self.store.set_value(it, 2, "%.1f" % (int(p * 1000)/10))
 
    def close(self):
        self.win.destroy()
 
 
class PeerListWindow(object):
 
    def __init__(self, torrent_name, closefunc):
        self.win = Window()
        self.win.connect("destroy", closefunc)
        self.win.set_title( _('Peers for "%s"')%torrent_name)
        self.sw = gtk.ScrolledWindow()
        self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
        self.sw.set_shadow_type(gtk.SHADOW_IN)
        self.win.add(self.sw)
 
        column_header = [_("IP address"), _("Client"), _("Connection"), _("KB/s down"), _("KB/s up"), _("MB downloaded"), _("MB uploaded"), _("% complete"), _("KB/s est. peer download")]
        pre_size_list = ['666.666.666.666', 'TorrentStorm 1.3', 'bad peer', 66666, 66666, '1666.66', '1666.66', '100.0', 6666]
        numeric_cols = [3,4,5,6,7,8]
        store_types = [gobject.TYPE_STRING]*3  + [gobject.TYPE_INT]*2 + [gobject.TYPE_STRING]*3 + [gobject.TYPE_INT]
 
        if advanced_ui:
            column_header[2:2] = [_("Peer ID")]
            pre_size_list[2:2] = ['-AZ2104-']
            store_types[2:2]   = [gobject.TYPE_STRING]
            column_header[5:5] = [_("Interested"),_("Choked"),_("Snubbed")]
            pre_size_list[5:5] = ['*','*','*']
            store_types[5:5]   = [gobject.TYPE_STRING]*3
            column_header[9:9] = [_("Interested"),_("Choked"),_("Optimistic upload")]
            pre_size_list[9:9] = ['*','*','*']
            store_types[9:9]   = [gobject.TYPE_STRING]*3
            numeric_cols = [4,8,12,13,14,15]
 
        num_columns = len(column_header)
        self.store = gtk.ListStore(*store_types)
        self.store.append(pre_size_list)
 
        def makesortfunc(sort_func):
            def sortfunc(treemodel, iter1, iter2, column):
                a_str = treemodel.get_value(iter1, column)
                b_str = treemodel.get_value(iter2, column)
                if a_str is not None and b_str is not None:
                    return sort_func(a_str,b_str)
                else:
                    return 0
            return sortfunc
 
        def ip_sort(a_str,b_str):
            for a,b in zip(a_str.split('.'), b_str.split('.')):
                if a == b:
                    continue
                if len(a) == len(b):
                    return cmp(a,b)
                return cmp(int(a), int(b))
            return 0
 
        def float_sort(a_str,b_str):
            a,b = 0,0
            try: a = float(a_str)
            except ValueError: pass
            try: b = float(b_str)
            except ValueError: pass
            return cmp(a,b)
 
        self.store.set_sort_func(0, makesortfunc(ip_sort), 0)
        for i in range(2,5):
            self.store.set_sort_func(num_columns-i, makesortfunc(float_sort), num_columns-i)
 
        self.treeview = gtk.TreeView(self.store)
        cs = []
        for i, name in enumerate(column_header):
            r = gtk.CellRendererText()
            if i in numeric_cols:
                r.set_property('xalign', 1)
            column = gtk.TreeViewColumn(name, r, text = i)
            column.set_resizable(True)
            column.set_min_width(5)
            column.set_sort_column_id(i)
            self.treeview.append_column(column)
            cs.append(column)
        self.treeview.set_rules_hint(True)
        self.sw.add(self.treeview)
        self.treeview.set_headers_visible(False)
        self.treeview.columns_autosize()
        self.sw.show_all()
        self.treeview.realize()
        for column in cs:
            column.set_fixed_width(column.get_width())
            column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
        self.treeview.set_headers_visible(True)
        self.store.clear()
        self.treeview.get_selection().set_mode(gtk.SELECTION_NONE)
        width = self.treeview.size_request()[0]
        self.win.set_default_size(width+SCROLLBAR_WIDTH, 300)
        self.win.show_all()
        self.prev = []
 
 
    def update(self, peers, bad_peers):
        fields = []
 
        def p_bool(value): return value and '*' or ''
 
        for peer in peers:
            field = []
            field.append(peer['ip']) 
 
            client, version = ClientIdentifier.identify_client(peer['id']) 
            field.append(client + ' ' + version)
 
            if advanced_ui:
                field.append(zurllib.quote(peer['id'])) 
 
            field.append(peer['initiation'] == 'R' and _("remote") or _("local"))
            dl = peer['download']
            ul = peer['upload']
 
            for l in (dl, ul):
                rate = l[1]
                if rate > 100:
                    field.append(int(round(rate/(2**10)))) 
                else:
                    field.append(0)
                if advanced_ui:
                    field.append(p_bool(l[2]))
                    field.append(p_bool(l[3]))
                    if len(l) > 4:
                        field.append(p_bool(l[4]))
                    else:
                        field.append(p_bool(peer['is_optimistic_unchoke']))
 
            field.append('%.2f'%round(dl[0] / 2**20, 2))
            field.append('%.2f'%round(ul[0] / 2**20, 2))
            field.append('%.1f'%round(int(peer['completed']*1000)/10, 1))
 
            field.append(int(peer['speed']//(2**10)))
 
            fields.append(field)
 
        for (ip, (is_banned, stats)) in bad_peers.iteritems():
            field = []
            field.append(ip)
 
            client, version = ClientIdentifier.identify_client(stats.peerid)
            field.append(client + ' ' + version)
 
            if advanced_ui:
                field.append(zurllib.quote(stats.peerid))
 
            field.append(_("bad peer"))
 
            # the sortable peer list won't take strings in these fields
            field.append(0) 
 
            if advanced_ui:
                field.extend([0] * 7) # upRate, * fields
            else:
                field.extend([0] * 1) # upRate
 
            field.append(_("%d ok") % stats.numgood)
            field.append(_("%d bad") % len(stats.bad))
            if is_banned: # completion
                field.append(_("banned"))
            else:
                field.append(_("ok"))
            field.append(0) # peer dl rate
            fields.append(field)
 
        if self.store.get_sort_column_id() < 0:
            # ListStore is unsorted, it might be faster to set only modified fields
            it = self.store.get_iter_first()
            for old, new in itertools.izip(self.prev, fields):
                if old != new:
                    for i, value in enumerate(new):
                        if value != old[i]:
                            self.store.set_value(it, i, value)
                it = self.store.iter_next(it)
            for i in range(len(fields), len(self.prev)):
                self.store.remove(it)
            for i in range(len(self.prev), len(fields)):
                self.store.append(fields[i])
            self.prev = fields
        else:
            # ListStore is sorted, no reason not to to reset all fields
            self.store.clear()
            for field in fields:
                self.store.append(field)
 
 
 
    def close(self):
        self.win.destroy()
 
 
class TorrentInfoWindow(object):
 
    def __init__(self, torrent_box, closefunc):
        self.win = Window()
        self.torrent_box = torrent_box
        name = self.torrent_box.metainfo.name
        self.win.set_title(_('Info for "%s"')%name)
        self.win.set_size_request(-1,-1)
        self.win.set_border_width(SPACING)
        self.win.set_resizable(False)
        self.win.connect('destroy', closefunc)
        self.vbox = gtk.VBox(spacing=SPACING)
 
        self.table = gtk.Table(rows=4, columns=3, homogeneous=False)
        self.table.set_row_spacings(SPACING)
        self.table.set_col_spacings(SPACING)
        y = 0
 
        def add_item(key, val, y):
            self.table.attach(ralign(gtk.Label(key)), 0, 1, y, y+1)
            v = gtk.Label(val)
            v.set_selectable(True)
            self.table.attach(lalign(v), 1, 2, y, y+1)
 
        add_item(_("Torrent name:"), name, y)
        y+=1
 
        announce = ''
        if self.torrent_box.metainfo.is_trackerless:
            announce = _("(trackerless torrent)")
        else:
            announce = self.torrent_box.metainfo.announce
        add_item(_("Announce url:"), announce, y)
        y+=1
 
        size = Size(self.torrent_box.metainfo.total_bytes)
        num_files = _(", in one file")
        if self.torrent_box.is_batch:
            num_files = _(", in %d files") % len(self.torrent_box.metainfo.sizes)
        add_item(_("Total size:"),  str(size)+num_files, y)
        y+=1
 
        if advanced_ui:
            pl = self.torrent_box.metainfo.piece_length
            count, lastlen = divmod(size, pl)
            sizedetail = '%d x %d + %d = %d' % (count, pl, lastlen, int(size))
            add_item(_("Pieces:"), sizedetail, y)
            y+=1
            add_item(_("Info hash:"), self.torrent_box.infohash.encode('hex'), y)
            y+=1
 
        path = self.torrent_box.dlpath
        filename = ''
        if path is None:
            path = ''
        else:
            if not self.torrent_box.is_batch:
                path,filename = os.path.split(self.torrent_box.dlpath)
            if path[-1] != os.sep:
                path += os.sep
            path = path_wrap(path)
        add_item(_("Save in:"), path, y)
        y+=1
 
        if not self.torrent_box.is_batch:
            add_item(_("File name:"), path_wrap(filename), y)
            y+=1
 
        self.vbox.pack_start(self.table)        
 
        if self.torrent_box.metainfo.comment not in (None, ''):
            commentbuffer = gtk.TextBuffer()
            commentbuffer.set_text(self.torrent_box.metainfo.comment)
            commenttext = gtk.TextView(commentbuffer)
            commenttext.set_editable(False)
            commenttext.set_cursor_visible(False)
            commenttext.set_wrap_mode(gtk.WRAP_WORD)
            commentscroll = gtk.ScrolledWindow()
            commentscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
            commentscroll.set_shadow_type(gtk.SHADOW_IN)
            commentscroll.add(commenttext)
            self.vbox.pack_start(commentscroll)
 
        self.vbox.pack_start(gtk.HSeparator(), expand=False, fill=False)
 
        self.hbox = gtk.HBox(spacing=SPACING)
        lbbox = gtk.HButtonBox()
        rbbox = gtk.HButtonBox()
        lbbox.set_spacing(SPACING)
 
        if LaunchPath.can_launch_files:
            opendirbutton = IconButton(_("_Open directory"), stock=gtk.STOCK_OPEN)
            opendirbutton.connect('clicked', self.torrent_box.open_dir)
            lbbox.pack_start(opendirbutton, expand=False, fill=False)
            opendirbutton.set_sensitive(self.torrent_box.can_open_dir())
 
        filelistbutton = IconButton(_("Show _file list"), stock='gtk-index')
        if self.torrent_box.is_batch:
            filelistbutton.connect('clicked', self.torrent_box.open_filelist)
        else:
            filelistbutton.set_sensitive(False)
        lbbox.pack_start(filelistbutton, expand=False, fill=False)
 
        closebutton = gtk.Button(stock='gtk-close')
        closebutton.connect('clicked', lambda w: self.close())
        rbbox.pack_end(closebutton, expand=False, fill=False)
 
        self.hbox.pack_start(lbbox, expand=False, fill=False)
        self.hbox.pack_end(  rbbox, expand=False, fill=False)
 
        self.vbox.pack_end(self.hbox, expand=False, fill=False)
 
        self.win.add(self.vbox)
 
        self.win.show_all()
 
    def close(self):
        self.win.destroy()
 
 
class TorrentBox(gtk.EventBox):
    torrent_tip_format = '%s:\n %s\n %s'
 
    def __init__(self, infohash, metainfo, dlpath, completion, main):
        gtk.EventBox.__init__(self)
        self.infohash = infohash
        self.metainfo = metainfo
        self.completion = completion
        self.main = main
 
        self.main_torrent_dnd_tip = _("drag to reorder")
        self.torrent_menu_tip = _("right-click for menu")
 
        self.set_save_location(dlpath)
 
        self.uptotal   = self.main.torrents[self.infohash].uptotal
        self.downtotal = self.main.torrents[self.infohash].downtotal
        if self.downtotal > 0:
            self.up_down_ratio = self.uptotal / self.metainfo.total_bytes
        else:
            self.up_down_ratio = None
 
        self.infowindow = None
        self.filelistwindow = None
        self.is_batch = metainfo.is_batch
        self.menu = None
        self.menu_handler = None
 
        self.vbox = gtk.VBox(homogeneous=False, spacing=SPACING)
        self.label = gtk.Label()
        self.set_name()
 
        self.vbox.pack_start(lalign(self.label), expand=False, fill=False)
 
        self.hbox = gtk.HBox(homogeneous=False, spacing=SPACING)
 
        self.icon = gtk.Image()
        self.icon.set_size_request(-1, 29)
 
        self.iconbox = gtk.VBox()
        self.iconevbox = gtk.EventBox()        
        self.iconevbox.add(self.icon)
        self.iconbox.pack_start(self.iconevbox, expand=False, fill=False)
        self.hbox.pack_start(self.iconbox, expand=False, fill=False)
 
        self.vbox.pack_start(self.hbox)
 
        self.infobox = gtk.VBox(homogeneous=False)
 
        self.progressbarbox = gtk.HBox(homogeneous=False, spacing=SPACING)
        self.progressbar = gtk.ProgressBar()
 
        self.reset_progressbar_color()
 
        if self.completion is not None:
            self.progressbar.set_fraction(self.completion)
            if self.completion >= 1:
                done_label = self.make_done_label()
                self.progressbar.set_text(done_label)
            else:
                self.progressbar.set_text('%.1f%%'%(self.completion*100))
        else:
            self.progressbar.set_text('?')
 
        self.progressbarbox.pack_start(self.progressbar,
                                       expand=True, fill=True)
 
        self.buttonevbox = gtk.EventBox()
        self.buttonbox = gtk.HBox(homogeneous=True, spacing=SPACING)
 
        self.infobutton = gtk.Button()
        self.infoimage = gtk.Image()
        self.infoimage.set_from_stock('bt-info', gtk.ICON_SIZE_BUTTON)
        self.infobutton.add(self.infoimage)
        self.infobutton.connect('clicked', self.open_info)
        self.main.tooltips.set_tip(self.infobutton,
                                   _("Torrent info"))
 
        self.buttonbox.pack_start(self.infobutton, expand=True)
 
        self.cancelbutton = gtk.Button()
        self.cancelimage = gtk.Image()
        if self.completion is not None and self.completion >= 1:
            self.cancelimage.set_from_stock('bt-remove', gtk.ICON_SIZE_BUTTON)
            self.main.tooltips.set_tip(self.cancelbutton,
                                       _("Remove torrent"))
        else:
            self.cancelimage.set_from_stock('bt-abort', gtk.ICON_SIZE_BUTTON)
            self.main.tooltips.set_tip(self.cancelbutton,
                                       _("Abort torrent"))
 
        self.cancelbutton.add(self.cancelimage)
        # not using 'clicked' because we want to check for CTRL key
        self.cancelbutton.connect('button-release-event', self.confirm_remove)
 
        self.buttonbox.pack_start(self.cancelbutton, expand=True, fill=False)
        self.buttonevbox.add(self.buttonbox)
 
        vbuttonbox = gtk.VBox(homogeneous=False)
        vbuttonbox.pack_start(self.buttonevbox, expand=False, fill=False)
        self.hbox.pack_end(vbuttonbox, expand=False, fill=False)
 
        self.infobox.pack_start(self.progressbarbox, expand=False, fill=False)
 
        self.hbox.pack_start(self.infobox, expand=True, fill=True)
        self.add( self.vbox )
 
        self.drag_source_set(gtk.gdk.BUTTON1_MASK,
                             TARGET_ALL,
                             gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY)
        self.connect('drag_data_get', self.drag_data_get)
 
        self.connect('drag_begin' , self.drag_begin )
        self.connect('drag_end'   , self.drag_end   )
        self.cursor_handler_id = self.connect('enter_notify_event', self.change_cursors)
 
 
    def set_save_location(self, dlpath):
        self.dlpath = dlpath
        updater_infohash = self.main.updater.infohash
        if updater_infohash == self.infohash:
            my_installer_dir = os.path.split(self.dlpath)[0]
            if self.main.updater.installer_dir != my_installer_dir:
                self.main.updater.set_installer_dir(my_installer_dir)
 
 
    def reset_progressbar_color(self):
        # Hack around broken GTK-Wimp theme:
        # make progress bar text always black
        # see task #694
        if is_frozen_exe and self.main.config['progressbar_hack']:
            style = self.progressbar.get_style().copy()
            black = style.black
            self.progressbar.modify_fg(gtk.STATE_PRELIGHT, black)
 
 
    def change_cursors(self, *args):
        # BUG: this is in a handler that is disconnected because the
        # window attributes are None until after show_all() is called
        self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
        self.buttonevbox.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
        self.disconnect(self.cursor_handler_id)
 
 
    def drag_data_get(self, widget, context, selection, targetType, eventTime):
        selection.set(selection.target, 8, self.infohash)
 
    def drag_begin(self, *args):
        pass
 
    def drag_end(self, *args):
        self.main.drag_end()
 
    def make_done_label(self, statistics=None):
        s = ''
        if statistics and statistics['timeEst'] is not None:
            s = _(", will seed for %s") % Duration(statistics['timeEst'])
        elif statistics:
            s = _(", will seed indefinitely.")
 
        if self.up_down_ratio is not None:
            done_label = _("Done, share ratio: %d%%") % \
                         (self.up_down_ratio*100) + s
        elif statistics is not None:
            done_label = _("Done, %s uploaded") % \
                         Size(statistics['upTotal']) + s
        else:
            done_label = _("Done")
 
        return done_label
 
 
    def set_name(self):
        self.label.set_text(self.metainfo.name)
        self.label.set_ellipsize(pango.ELLIPSIZE_END)
 
    def make_menu(self, extra_menu_items=[]):
        if self.menu_handler:
            self.disconnect(self.menu_handler)
 
        ## Basic Info
        menu_items = [ MenuItem(_("Torrent _info"   ), func=self.open_info), ]
        open_dir_func = None
        if LaunchPath.can_launch_files and self.can_open_dir():
            open_dir_func = self.open_dir
        menu_items.append( MenuItem(_("_Open directory" ), func=open_dir_func) )
        filelistfunc = None
        if self.is_batch:
            filelistfunc = self.open_filelist
        menu_items.append(MenuItem(_("_File list"), func=filelistfunc))
        if self.torrent_state == RUNNING:
            menu_items.append(MenuItem(_("_Peer list"), func=self.open_peerlist))
        ## end Basic Info
 
        menu_items.append(gtk.SeparatorMenuItem())
 
        ## Settings
        # change save location
        change_save_location_func = None
        if self.torrent_state != RUNNING and self.completion <= 0:
            change_save_location_func = self.change_save_location
        menu_items.append(MenuItem(_("_Change location"),
                                   func=change_save_location_func))
        # seed forever item
        self.seed_forever_item = gtk.CheckMenuItem(_("_Seed indefinitely"))
        self.reset_seed_forever()
        def sft(widget, *args):
            active = widget.get_active()
            infohash = self.infohash
            for option in ('seed_forever', 'seed_last_forever'):
                self.main.torrentqueue.set_config(option, active, infohash)
                self.main.torrentqueue.set_config(option, active, infohash)
        self.seed_forever_item.connect('toggled', sft)
        menu_items.append(self.seed_forever_item)
        ## end Settings
 
        menu_items.append(gtk.SeparatorMenuItem())
 
        ## Queue state dependent items
        if self.torrent_state == KNOWN:
            menu_items.append( MenuItem(_("Re_start"), func=self.move_to_end   ))
        elif self.torrent_state == QUEUED:
            #Here's where we'll put the "Start hash check" menu item
            menu_items.append(MenuItem(_("Download _now"), func=self.start))            
        elif self.torrent_state in (RUNNING, RUN_QUEUED):
            # no items for here
            pass
 
        ## Completion dependent items
        if self.completion is not None and self.completion >= 1:
            if self.torrent_state != KNOWN:
                menu_items.append(MenuItem(_("_Finish"), func=self.finish))
            menu_items.append( MenuItem(_("_Remove" ), func=self.confirm_remove))
        else:
            if self.torrent_state in (RUNNING, RUN_QUEUED):
                menu_items.append(MenuItem(_("Download _later"), func=self.move_to_end))
            else:
                #Here's where we'll put the "Seed _later" menu item
                pass
            menu_items.append(MenuItem(_("_Abort" ), func=self.confirm_remove))
 
        ## build the menu
        self.menu = gtk.Menu()
 
        for i in menu_items:
            i.show()
            self.menu.add(i)
 
        self.menu_handler = self.connect_object("event", self.show_menu, self.menu)
 
    def reset_seed_forever(self):
        sfb = False
        d = self.main.torrents[self.infohash].config.getDict()
        if d.has_key('seed_forever'):
            sfb = d['seed_forever']
        self.seed_forever_item.set_active(bool(sfb))        
 
    def change_save_location(self, widget=None):
        self.main.change_save_location(self.infohash)
 
    def open_info(self, widget=None):
        if self.infowindow is None:
            self.infowindow = TorrentInfoWindow(self, self.infoclosed)
 
    def infoclosed(self, widget=None):
        self.infowindow = None
 
    def close_info(self):
        if self.infowindow is not None:
            self.infowindow.close()
 
    def open_filelist(self, widget):
        if not self.is_batch:
            return
        if self.filelistwindow is None:
            self.filelistwindow = FileListWindow(self.metainfo,
                                                 self.filelistclosed)
            self.main.torrentqueue.check_completion(self.infohash, True)
 
    def filelistclosed(self, widget):
        self.filelistwindow = None
 
    def close_filelist(self):
        if self.filelistwindow is not None:
            self.filelistwindow.close()
 
    def close_child_windows(self):
        self.close_info()
        self.close_filelist()
 
    def destroy(self):
        if self.menu is not None:
            self.menu.destroy()
        self.menu = None
        gtk.EventBox.destroy(self)
 
    def show_menu(self, widget, event):
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
            widget.popup(None, None, None, event.button, event.time)
            return True
        return False
 
    def _short_path(self, dlpath):
        path_length = 40
        sep = '...'
        ret = os.path.split(dlpath)[0]
        if len(ret) > path_length+len(sep):
            return ret[:int(path_length/2)]+sep+ret[-int(path_length/2):]
        else:
            return ret
 
    def get_path_to_open(self):
        path = self.dlpath
        if not self.is_batch:
            path = os.path.split(self.dlpath)[0]
        return path
 
    def can_open_dir(self):
        return os.access(self.get_path_to_open(), os.F_OK|os.R_OK)
 
    def open_dir(self, widget):
        LaunchPath.launchdir(self.get_path_to_open())
 
    def confirm_remove(self, widget, event=None):
        if event is not None and event.get_state() & gtk.gdk.CONTROL_MASK:
            self.remove()
        else:
            message = _('Are you sure you want to remove "%s"?') % self.metainfo.name
            if self.completion >= 1:
                if self.up_down_ratio is not None:
                    message = _("Your share ratio for this torrent is %d%%. ")%(self.up_down_ratio*100) + message
                else:
                    message = _("You have uploaded %s to this torrent. ")%(Size(self.uptotal)) + message
 
            d = MessageDialog(self.main.mainwindow,
                              _("Remove this torrent?"),
                              message, 
                              type=gtk.MESSAGE_QUESTION,
                              buttons=gtk.BUTTONS_OK_CANCEL,
                              yesfunc=self.remove,
                              default=gtk.RESPONSE_OK,
                              )
 
    def remove(self):
        self.main.torrentqueue.remove_torrent(self.infohash)
 
 
class KnownTorrentBox(TorrentBox):
 
    torrent_state = KNOWN
 
    def __init__(self, infohash, metainfo, dlpath, completion, main):
        TorrentBox.__init__(self, infohash, metainfo, dlpath, completion, main)
 
        status_tip = ''
        if completion >= 1:
            self.icon.set_from_stock('bt-finished', gtk.ICON_SIZE_LARGE_TOOLBAR)
            status_tip = _("Finished")
            known_torrent_dnd_tip = _("drag into list to seed")
        else:
            self.icon.set_from_stock('bt-broken', gtk.ICON_SIZE_LARGE_TOOLBAR)
            status_tip = _("Failed")
            known_torrent_dnd_tip = _("drag into list to resume")
 
        self.main.tooltips.set_tip(self.iconevbox,
                                   self.torrent_tip_format % (status_tip,
                                                              known_torrent_dnd_tip,
                                                              self.torrent_menu_tip))
        self.make_menu()
        self.show_all()
 
    def move_to_end(self, widget):
        self.main.change_torrent_state(self.infohash, QUEUED)
 
 
class DroppableTorrentBox(TorrentBox):
 
    def __init__(self, infohash, metainfo, dlpath, completion, main):
        TorrentBox.__init__(self, infohash, metainfo, dlpath, completion, main)
 
        self.drag_dest_set(gtk.DEST_DEFAULT_DROP,
                           TARGET_ALL,
                           gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY)
 
        self.connect('drag_data_received', self.drag_data_received)
        self.connect('drag_motion', self.drag_motion)
        self.index = None
 
    def drag_data_received(self, widget, context, x, y, selection, targetType, time):
        if targetType == BT_TARGET_TYPE:
            half_height = self.size_request()[1] // 2
            where = cmp(y, half_height)
            if where == 0: where = 1
            self.parent.put_infohash_at_child(selection.data, self, where)
        else:
            self.main.accept_dropped_file(widget, context, x, y, selection, targetType, time)
 
    def drag_motion(self, widget, context, x, y, time):
        self.get_current_index()
        half_height = self.size_request()[1] // 2
        if y < half_height: 
            self.parent.highlight_before_index(self.index)
        else:
            self.parent.highlight_after_index(self.index)
        return False
 
    def drag_end(self, *args):
        self.parent.highlight_child()
        TorrentBox.drag_end(self, *args)
 
    def get_current_index(self):
        self.index = self.parent.get_index_from_child(self)
 
 
class QueuedTorrentBox(DroppableTorrentBox):
    icon_name = 'bt-queued'
    torrent_state = QUEUED
 
    def __init__(self, infohash, metainfo, dlpath, completion, main):
        DroppableTorrentBox.__init__(self, infohash, metainfo, dlpath, completion, main)
 
        self.state_name = _("Waiting")
        self.main.tooltips.set_tip(self.iconevbox,
                                   self.torrent_tip_format % (self.state_name,
                                                              self.main_torrent_dnd_tip,
                                                              self.torrent_menu_tip))
 
        self.icon.set_from_stock(self.icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR)
        self.make_menu()
        self.show_all()
 
    def start(self, widget):
        self.main.runbox.put_infohash_last(self.infohash)
 
    def finish(self, widget):
        self.main.change_torrent_state(self.infohash, KNOWN)
 
 
class PausedTorrentBox(DroppableTorrentBox):
    icon_name = 'bt-paused'
    torrent_state = RUN_QUEUED
 
    def __init__(self, infohash, metainfo, dlpath, completion, main):
        DroppableTorrentBox.__init__(self, infohash, metainfo, dlpath, completion, main)
 
        self.state_name = _("Paused")
        self.main.tooltips.set_tip(self.iconevbox,
                                   self.torrent_tip_format % (self.state_name,
                                                              self.main_torrent_dnd_tip,
                                                              self.torrent_menu_tip))
 
        self.icon.set_from_stock(self.icon_name, gtk.ICON_SIZE_LARGE_TOOLBAR)
        self.make_menu()
        self.show_all()
 
    def move_to_end(self, widget):
        self.main.change_torrent_state(self.infohash, QUEUED)
 
    def finish(self, widget):
        self.main.change_torrent_state(self.infohash, KNOWN)
 
    def update_status(self, statistics):
        # in case the TorrentQueue thread calls widget.update_status()
        # before the GUI has changed the torrent widget to a
        # RunningTorrentBox
        pass
 
 
class RunningTorrentBox(PausedTorrentBox):
    torrent_state = RUNNING
 
    def __init__(self, infohash, metainfo, dlpath, completion, main):
        DroppableTorrentBox.__init__(self, infohash, metainfo, dlpath, completion, main)
 
        self.main.tooltips.set_tip(self.iconevbox,
                                   self.torrent_tip_format % (_("Running"),
                                                              self.main_torrent_dnd_tip,
                                                              self.torrent_menu_tip))
 
        self.seed = False
        self.peerlistwindow = None
        self.update_peer_list_flag = 0
 
        self.icon.set_from_stock('bt-running', gtk.ICON_SIZE_LARGE_TOOLBAR)
 
        self.rate_label_box = gtk.HBox(homogeneous=True)
 
        self.up_rate   = gtk.Label()
        self.down_rate = gtk.Label()
        self.rate_label_box.pack_start(lalign(self.up_rate  ),
                                       expand=True, fill=True)
        self.rate_label_box.pack_start(lalign(self.down_rate),
                                       expand=True, fill=True)
 
        self.infobox.pack_start(self.rate_label_box)        
 
        if advanced_ui:
            self.extrabox = gtk.VBox(homogeneous=False)
            #self.extrabox = self.vbox
 
            self.up_curr    = FancyLabel(_("Current up: %s"  ), 0)
            self.down_curr  = FancyLabel(_("Current down: %s"), 0)
            self.curr_box   = gtk.HBox(homogeneous=True)
            self.curr_box.pack_start(lalign(self.up_curr  ), expand=True, fill=True)
            self.curr_box.pack_start(lalign(self.down_curr), expand=True, fill=True)
            self.extrabox.pack_start(self.curr_box)
 
            self.up_prev    = FancyLabel(_("Previous up: %s"  ), 0)
            self.down_prev  = FancyLabel(_("Previous down: %s"), 0)
            self.prev_box   = gtk.HBox(homogeneous=True)
            self.prev_box.pack_start(lalign(self.up_prev  ), expand=True, fill=True)
            self.prev_box.pack_start(lalign(self.down_prev), expand=True, fill=True)
            self.extrabox.pack_start(self.prev_box)
 
            self.share_ratio = FancyLabel(_("Share ratio: %0.02f%%"), 0)
            self.extrabox.pack_start(lalign(self.share_ratio))
 
            self.peer_info = FancyLabel(_("%s peers, %s seeds. Totals from "
                                          "tracker: %s"), 0, 0, 'NA')
            self.extrabox.pack_start(lalign(self.peer_info))
 
            self.dist_copies = FancyLabel(_("Distributed copies: %d; Next: %s"), 0, '')
            self.extrabox.pack_start(lalign(self.dist_copies))
 
            self.piece_info = FancyLabel(_("Pieces: %d total, %d complete, "
                                           "%d partial, %d active (%d empty)"), *(0,)*5)
            self.extrabox.pack_start(lalign(self.piece_info))
 
            self.bad_info = FancyLabel(_("%d bad pieces + %s in discarded requests"), 0, 0)
            self.extrabox.pack_start(lalign(self.bad_info))
 
            # extra info
 
            pl = self.metainfo.piece_length
            tl = self.metainfo.total_bytes
            count, lastlen = divmod(tl, pl)
            self.piece_count = count + (lastlen > 0)
 
            self.infobox.pack_end(self.extrabox, expand=False, fill=False)
 
        self.make_menu()
        self.show_all() 
 
 
    def change_to_completed(self):
        self.completion = 1.0
        self.cancelimage.set_from_stock('bt-remove', gtk.ICON_SIZE_BUTTON)
        self.main.tooltips.set_tip(self.cancelbutton,
                                   _("Remove torrent"))
 
        updater_infohash = self.main.updater.infohash
        if updater_infohash == self.infohash:
            self.main.updater.start_install()
 
        self.make_menu()
 
    def close_child_windows(self):
        TorrentBox.close_child_windows(self)
        self.close_peerlist()
 
    def open_filelist(self, widget):
        if not self.is_batch:
            return
        if self.filelistwindow is None:
            self.filelistwindow = FileListWindow(self.metainfo,
                                                 self.filelistclosed)
            self.main.make_statusrequest()
 
    def open_peerlist(self, widget):
        if self.peerlistwindow is None:
            self.peerlistwindow = PeerListWindow(self.metainfo.name,
                                                 self.peerlistclosed)
            self.main.make_statusrequest()
 
    def peerlistclosed(self, widget):
        self.peerlistwindow = None
        self.update_peer_list_flag = 0
 
    def close_peerlist(self):
        if self.peerlistwindow is not None:
            self.peerlistwindow.close()
 
    rate_label = ': %s'
    eta_label = '?'
    done_label = _("Done")
    progress_bar_label = _("%.1f%% done, %s remaining")
    down_rate_label = _("Download rate")
    up_rate_label = _("Upload rate"  )
 
    def update_status(self, statistics):
        fractionDone = statistics.get('fractionDone')
        activity = statistics.get('activity')
 
        self.main.set_title(torrentName=self.metainfo.name,
                            fractionDone=fractionDone)
 
        dt = self.downtotal
        if statistics.has_key('downTotal'):
            dt += statistics['downTotal']
 
        ut = self.uptotal
        if statistics.has_key('upTotal'):
            ut += statistics['upTotal']
 
        if dt > 0:
            self.up_down_ratio = ut / self.metainfo.total_bytes
 
        done_label = self.done_label
        eta_label = self.eta_label
        if 'numPeers' in statistics:
            eta = statistics.get('timeEst')
            if eta is not None:
                eta_label = Duration(eta)
            if fractionDone == 1:
                done_label = self.make_done_label(statistics)
 
        if fractionDone == 1:
            self.progressbar.set_fraction(1)
            self.progressbar.set_text(done_label)
            self.reset_seed_forever()
            if not self.completion >= 1:
                self.change_to_completed()
        else:
            self.progressbar.set_fraction(fractionDone)
            progress_bar_label = self.progress_bar_label % \
                                 (int(fractionDone*1000)/10, eta_label) 
            self.progressbar.set_text(progress_bar_label)
 
 
        if 'numPeers' not in statistics:
            return
 
        self.down_rate.set_text(self.down_rate_label+self.rate_label %
                                Rate(statistics['downRate']))
        self.up_rate.set_text  (self.up_rate_label+self.rate_label %
                                Rate(statistics['upRate']))
 
        if advanced_ui:
            if self.up_down_ratio is not None:
                self.share_ratio.set_value(self.up_down_ratio*100)
 
            num_seeds = statistics['numSeeds']
            if self.seed:
                num_seeds = statistics['numOldSeeds'] = 0 # !@# XXX
 
            if statistics['trackerPeers'] is not None:
                totals = '%d/%d' % (statistics['trackerPeers'],
                                    statistics['trackerSeeds'])
            else:
                totals = _("NA")
            self.peer_info.set_value(statistics['numPeers'], num_seeds, totals)
 
            self.up_curr.set_value(str(Size(statistics['upTotal'])))
            self.down_curr.set_value(str(Size(statistics['downTotal'])))
 
            self.up_prev.set_value(str(Size(self.uptotal)))
            self.down_prev.set_value(str(Size(self.downtotal)))
 
            # refresh extra info
            self.piece_info.set_value(self.piece_count,
                                      statistics['storage_numcomplete'],
                                      statistics['storage_dirty'],
                                      statistics['storage_active'],
                                      statistics['storage_new'] )
 
            self.dist_copies.set_value( statistics['numCopies'], ', '.join(["%d:%.1f%%" % (a, int(b*1000)/10) for a, b in zip(itertools.count(int(statistics['numCopies']+1)), statistics['numCopyList'])]))
 
            self.bad_info.set_value(statistics['storage_numflunked'], Size(statistics['discarded']))
 
        if self.peerlistwindow is not None:
            if self.update_peer_list_flag == 0:
                spew = statistics.get('spew')
                if spew is not None:
                    self.peerlistwindow.update(spew, statistics['bad_peers'])
            self.update_peer_list_flag = (self.update_peer_list_flag + 1) % 4
        if self.filelistwindow is not None:
            if 'files_left' in statistics:
                self.filelistwindow.update(statistics['files_left'],
                                           statistics['files_allocated'])
 
 
class DroppableHSeparator(PaddedHSeparator):
 
    def __init__(self, box, spacing=SPACING):
        PaddedHSeparator.__init__(self, spacing)
        self.box = box
        self.main = box.main
 
        self.drag_dest_set(gtk.DEST_DEFAULT_DROP,
                           TARGET_ALL,
                           gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY)
 
        self.connect('drag_data_received', self.drag_data_received)
        self.connect('drag_motion'       , self.drag_motion       )
 
    def drag_highlight(self):
        self.sep.drag_highlight()
        self.main.add_unhighlight_handle()
 
    def drag_unhighlight(self):
        self.sep.drag_unhighlight()
 
    def drag_data_received(self, widget, context, x, y, selection, targetType, time):
        if targetType == BT_TARGET_TYPE:
            self.box.drop_on_separator(self, selection.data)
        else:
            self.main.accept_dropped_file(widget, context, x, y, selection, targetType, time)
 
    def drag_motion(self, wid, context, x, y, time):
        self.drag_highlight()
        return False
 
 
class DroppableBox(HSeparatedBox):
    def __init__(self, main, spacing=0):
        HSeparatedBox.__init__(self, spacing=spacing)
        self.main = main
        self.drag_dest_set(gtk.DEST_DEFAULT_DROP,
                           TARGET_ALL,
                           gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY)
        self.connect('drag_data_received', self.drag_data_received)
        self.connect('drag_motion', self.drag_motion)
 
    def drag_motion(self, widget, context, x, y, time):
        return False
 
    def drag_data_received(self, widget, context, x, y, selection, targetType, time):
        pass
 
 
class KnownBox(DroppableBox):
 
    def __init__(self, main, spacing=0):
        DroppableBox.__init__(self, main, spacing=spacing)
        self.drag_dest_set(gtk.DEST_DEFAULT_DROP,
                           TARGET_ALL,
                           gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY)
 
    def pack_start(self, widget, *args, **kwargs):
        old_len = len(self.get_children())
        DroppableBox.pack_start(self, widget, *args, **kwargs)
        if old_len <= 0:
            self.main.maximize_known_pane()
        self.main.knownscroll.scroll_to_bottom()
 
    def remove(self, widget):
        DroppableBox.remove(self, widget)
        new_len = len(self.get_children())
        if new_len == 0:
            self.main.maximize_known_pane()
 
    def drag_data_received(self, widget, context, x, y, selection, targetType, time):
        if targetType == BT_TARGET_TYPE:
            infohash = selection.data
            self.main.finish(infohash)
        else:
            self.main.accept_dropped_file(widget, context, x, y, selection, targetType, time)
 
    def drag_motion(self, widget, context, x, y, time):
        self.main.drag_highlight(widget=self)
 
    def drag_highlight(self):
        self.main.knownscroll.drag_highlight()
        self.main.add_unhighlight_handle()
 
    def drag_unhighlight(self):
        self.main.knownscroll.drag_unhighlight()
 
 
class RunningAndQueueBox(gtk.VBox):
 
    def __init__(self, main, **kwargs):
        gtk.VBox.__init__(self, **kwargs)
        self.main = main
 
    def drop_on_separator(self, sep, infohash):
        self.main.change_torrent_state(infohash, QUEUED, 0)
 
    def highlight_between(self):
        self.drag_highlight()
 
    def drag_highlight(self):
        self.get_children()[1].drag_highlight()
 
    def drag_unhighlight(self):
        self.get_children()[1].drag_unhighlight()
 
 
class SpacerBox(DroppableBox):
 
    def drag_data_received(self, widget, context, x, y, selection, targetType, time):
        if targetType == BT_TARGET_TYPE:
            infohash = selection.data
            self.main.queuebox.put_infohash_last(infohash)
        else:
            self.main.accept_dropped_file(widget, context, x, y, selection, targetType, time)
 
        return True
 
BEFORE = -1
AFTER  =  1
 
class ReorderableBox(DroppableBox):
 
    def new_separator(self):
        return DroppableHSeparator(self)
 
    def __init__(self, main):
        DroppableBox.__init__(self, main)
        self.main = main
 
        self.drag_dest_set(gtk.DEST_DEFAULT_DROP,
                           TARGET_ALL,
                           gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY)
 
        self.connect('drag_data_received', self.drag_data_received)
        self.connect('drag_motion'       , self.drag_motion)
 
    def drag_data_received(self, widget, context, x, y, selection, targetType, time):
 
        if targetType == BT_TARGET_TYPE:
            half_height = self.size_request()[1] // 2
            if y < half_height:
                self.put_infohash_first(selection.data)
            else:
                self.put_infohash_last(selection.data)
        else:
            self.main.accept_dropped_file(widget, context, x, y, selection, targetType, time)
        return True
 
    def drag_motion(self, widget, context, x, y, time):
        return False
 
    def drag_highlight(self):
        final = self.get_children()[-1]
        final.drag_highlight()
        self.main.add_unhighlight_handle()
 
    def drag_unhighlight(self): 
        self.highlight_child(index=None)
        self.parent.drag_unhighlight()
 
    def highlight_before_index(self, index):
        self.drag_unhighlight()
        children = self._get_children()
        if index > 0:
            children[index*2 - 1].drag_highlight()
        else:
            self.highlight_at_top()
 
    def highlight_after_index(self, index):
        self.drag_unhighlight()
        children = self._get_children()
        if index*2 < len(children)-1:
            children[index*2 + 1].drag_highlight()
        else:
            self.highlight_at_bottom()
 
    def highlight_child(self, index=None):
        for i, child in enumerate(self._get_children()):
            if index is not None and i == index*2:
                child.drag_highlight()
            else:
                child.drag_unhighlight()
 
 
    def drop_on_separator(self, sep, infohash):
        children = self._get_children()
        for i, child in enumerate(children):
            if child == sep:
                reference_child = children[i-1]
                self.put_infohash_at_child(infohash, reference_child, AFTER)
                break
 
 
    def get_queue(self):
        queue = []
        c = self.get_children()
        for t in c:
            queue.append(t.infohash)
        return queue
 
    def put_infohash_first(self, infohash):
        self.highlight_child()
        children = self.get_children()
        if len(children) > 1 and infohash == children[0].infohash:
            return
 
        self.put_infohash_at_index(infohash, 0)
 
    def put_infohash_last(self, infohash):
        self.highlight_child()
        children = self.get_children()
        end = len(children)
        if len(children) > 1 and infohash == children[end-1].infohash:
            return
 
        self.put_infohash_at_index(infohash, end)
 
    def put_infohash_at_child(self, infohash, reference_child, where):
        self.highlight_child()
        if infohash == reference_child.infohash:
            return
 
        target_index = self.get_index_from_child(reference_child)
        if where == AFTER:
            target_index += 1
        self.put_infohash_at_index(infohash, target_index)
 
    def get_index_from_child(self, child):
        c = self.get_children()
        ret = -1
        try:
            ret = c.index(child)
        except ValueError:
            pass
        return ret
 
    def highlight_at_top(self):
        raise NotImplementedError
 
    def highlight_at_bottom(self):
        raise NotImplementedError
 
    def put_infohash_at_index(self, infohash, end):
        raise NotImplementedError
 
class RunningBox(ReorderableBox):
 
    def put_infohash_at_index(self, infohash, target_index):
        #print 'RunningBox.put_infohash_at_index', infohash.encode('hex')[:8], target_index
 
        l = self.get_queue()
        replaced = None
        if l:
            replaced = l[-1]
        self.main.confirm_replace_running_torrent(infohash, replaced,
                                                  target_index)
 
    def highlight_at_top(self):
        pass
        # BUG: Don't know how I will indicate in the UI that the top of the list is highlighted
 
    def highlight_at_bottom(self):
        self.parent.highlight_between()
 
 
class QueuedBox(ReorderableBox):
 
    def put_infohash_at_index(self, infohash, target_index):
        #print 'want to put', infohash.encode('hex'), 'at', target_index
        self.main.change_torrent_state(infohash, QUEUED, target_index)
 
    def highlight_at_top(self):
        self.parent.highlight_between()
 
    def highlight_at_bottom(self):
        pass
        # BUG: Don't know how I will indicate in the UI that the bottom of the list is highlighted
 
 
 
class Struct(object):
    pass
 
 
class SearchField(gtk.Entry):
    def __init__(self, default_text, visit_url_func):
        gtk.Entry.__init__(self)
        self.default_text = default_text
        self.visit_url_func = visit_url_func
        self.set_text(self.default_text)
        self.set_size_request(150, -1)
 
        # default gtk Entry dnd processing is broken on linux!
        #  - default Motion handling causes asyncs
        #  - there's no way to filter the default text dnd
        # see the parent window for a very painful work-around
        self.drag_dest_unset()
 
        self.connect('key-press-event', self.check_for_enter)
        self.connect('button-press-event', self.begin_edit)
        self.search_completion = gtk.EntryCompletion()
        self.search_completion.set_text_column(0)
        self.search_store = gtk.ListStore(gobject.TYPE_STRING)
        self.search_completion.set_model(self.search_store)
        self.set_completion(self.search_completion)
        self.reset_text()
        self.timeout_id = None
 
    def begin_edit(self, *args):
        if self.get_text() == self.default_text:
            self.set_text('')
 
    def check_for_enter(self, widget, event):
        if event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
            self.search()
 
    def reset_text(self):
        self.set_text(self.default_text)
 
    def search(self, *args):
        search_term = self.get_text()
        if search_term and search_term != self.default_text:
            self.search_store.append([search_term])
            search_url = SEARCH_URL % {'query' :zurllib.quote(search_term),
                                       'client':'M-%s'%version.replace('.','-')}
 
            self.timeout_id = gobject.timeout_add(2000, self.resensitize)
            self.set_sensitive(False)
            self.visit_url_func(search_url, callback=self.resensitize)
        else:
            self.reset_text()
            self.select_region(0, -1)
            self.grab_focus()
 
    def resensitize(self):
        self.set_sensitive(True)
        self.reset_text()
        if self.timeout_id is not None:
            gobject.source_remove(self.timeout_id)
            self.timeout_id = None
 
 
class DownloadInfoFrame(object):
 
    def __init__(self, config, torrentqueue):
        self.config = config
        if self.config['save_in'] == '':
           self.config['save_in'] = smart_dir('')
 
        self.torrentqueue = torrentqueue
        self.torrents = {}
        self.running_torrents = {}
        self.lists = {}
        self.update_handle = None
        self.unhighlight_handle = None
        self.custom_size = False
        self.child_windows = {}
        self.postponed_save_windows = []
        self.helpwindow     = None
        self.errordialog    = None
 
        self.mainwindow = Window(gtk.WINDOW_TOPLEVEL)
 
        #tray icon
        self.trayicon = TrayIcon(not self.config['start_minimized'],
                                 toggle_func=self.toggle_shown,
                                 quit_func=self.quit)
        self.traythread = threading.Thread(target=self.trayicon.enable,
                                           args=())
        self.traythread.setDaemon(True)
 
        if os.name == "nt":
            # gtk has no way to check this?
            self.iconized = False
            self.mainwindow.connect('window-state-event', self.window_event) 
 
        if self.config['start_minimized']:
            self.mainwindow.iconify()
 
        gtk.threads_enter()
 
        self.mainwindow.set_border_width(0)
 
        self.set_seen_remote_connections(False)
        self.set_seen_connections(False)
 
        self.mainwindow.drag_dest_set(gtk.DEST_DEFAULT_ALL,
                                      TARGET_EXTERNAL,
                                      gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY)
 
        self.mainwindow.connect('drag_leave'        , self.drag_leave         )
        self.mainwindow.connect('drag_data_received', self.accept_dropped_file)
 
        self.mainwindow.set_size_request(WINDOW_WIDTH, -1)
 
        self.mainwindow.connect('destroy', self.cancel)
 
        self.mainwindow.connect('size-allocate', self.size_was_allocated)
 
        self.accel_group = gtk.AccelGroup()
 
        self.mainwindow.add_accel_group(self.accel_group)
 
        #self.accel_group.connect(ord('W'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_LOCKED,
        #                         lambda *args: self.mainwindow.destroy())
 
        self.tooltips = gtk.Tooltips()
 
        self.logbuffer = LogBuffer()
        self.log_text(_("%s started")%app_name, severity=None)
 
        self.box1 = gtk.VBox(homogeneous=False, spacing=0)
 
        self.box2 = gtk.VBox(homogeneous=False, spacing=0)
        self.box2.set_border_width(SPACING)
 
        self.menubar = gtk.MenuBar()
        self.box1.pack_start(self.menubar, expand=False, fill=False)
 
        self.ssbutton = StopStartButton(self)
 
        # keystrokes used: A D F H L N O P Q S U X (E)
 
        quit_menu_label = _("_Quit")
        if os.name == 'nt':
            quit_menu_label = _("E_xit")
 
        file_menu_items = ((_("_Open torrent file"), self.select_torrent_to_open),
                           (_("Open torrent _URL"), self.enter_url_to_open),
                           (_("Make _new torrent" ), self.make_new_torrent),
 
                           ('----'          , None),
                           (_("_Pause/Play"), self.ssbutton.toggle),
                           ('----'          , None),
                           (quit_menu_label , lambda w: self.mainwindow.destroy()),
                           )
        view_menu_items = ((_("Show/Hide _finished torrents"), self.toggle_known),
                           # BUG: if you reorder this menu, see def set_custom_size() first
                           (_("_Resize window to fit"), lambda w: self.resize_to_fit()),
                           ('----'             , None),
                           (_("_Log")          , lambda w: self.open_window('log')),
                           # 'View log of all download activity',
                           #('----'            , None),
                           (_("_Settings")     , lambda w: self.open_window('settings')),
                           #'Change download behavior and network settings',
                           )
        help_menu_items = ((_("_Help")         , self.open_help),
                           #(_("_Help Window") , lambda w: self.open_window('help')),
                           (_("_About")         , lambda w: self.open_window('about')),
                           (_("_Donate")        , lambda w: self.donate()),
                           #(_("Rais_e")        , lambda w: self.raiseerror()), 
                           )
 
        self.filemenu = gtk.MenuItem(_("_File"))
 
        self.filemenu.set_submenu(build_menu(file_menu_items, self.accel_group))
        self.filemenu.show()
 
        self.viewmenu = gtk.MenuItem(_("_View"))
        self.viewmenu.set_submenu(build_menu(view_menu_items, self.accel_group))
        self.viewmenu.show()
 
        self.helpmenu = gtk.MenuItem(_("_Help"))
        self.helpmenu.set_submenu(build_menu(help_menu_items, self.accel_group))
        self.helpmenu.show()
 
        if os.name != 'nt':
            self.helpmenu.set_right_justified(True)
 
        self.menubar.append(self.filemenu)
        self.menubar.append(self.viewmenu)
        self.menubar.append(self.helpmenu)
 
        self.menubar.show()
 
        self.header = gtk.HBox(homogeneous=False)
 
        self.box1.pack_start(self.box2, expand=False, fill=False)
 
        # control box: rate slider, start-stop button, search widget, status light
        self.controlbox = gtk.HBox(homogeneous=False)
 
        controlbox_padding = SPACING//2
 
        # stop-start button
        self.controlbox.pack_start(malign(self.ssbutton),
                                   expand=False, fill=False)
 
        # rate slider
        self.rate_slider_box = RateSliderBox(self.config, self.torrentqueue)
        self.controlbox.pack_start(self.rate_slider_box,
                                   expand=True, fill=True,
                                   padding=controlbox_padding)
 
        self.controlbox.pack_start(gtk.VSeparator(), expand=False, fill=False,
                                   padding=controlbox_padding)
 
        # search box 
        self.search_field = SearchField(_("Search for torrents"), self.visit_url)
        sfa = gtk.Alignment(xalign=0, yalign=0.5, xscale=1, yscale=0)
        sfa.add(self.search_field)
        self.controlbox.pack_start(sfa,
                                   expand=False, fill=False, padding=controlbox_padding)
 
        # separator
        self.controlbox.pack_start(gtk.VSeparator(), expand=False, fill=False,
                                   padding=controlbox_padding)
 
        # status light
        self.status_light = StatusLight(self)
 
        self.controlbox.pack_start(malign(self.status_light),
                                   expand=False, fill=False)
 
        self.box2.pack_start(self.controlbox,
                             expand=False, fill=False, padding=0)
        # end control box
 
        self.paned = gtk.VPaned()
 
        self.knownscroll = ScrolledWindow()
        self.knownscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
        self.knownscroll.set_shadow_type(gtk.SHADOW_NONE)
        self.knownscroll.set_size_request(-1, SPACING)
 
        self.knownbox = KnownBox(self)
        self.knownbox.set_border_width(SPACING)
 
        self.knownscroll.add_with_viewport(self.knownbox)
        self.paned.pack1(self.knownscroll, resize=False, shrink=True)
 
 
        self.mainscroll = AutoScrollingWindow()
        self.mainscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
        self.mainscroll.set_shadow_type(gtk.SHADOW_NONE)
        self.mainscroll.set_size_request(-1, SPACING)
 
        self.scrollbox = RunningAndQueueBox(self, homogeneous=False)
        self.scrollbox.set_border_width(SPACING)
 
        self.runbox = RunningBox(self)
        self.scrollbox.pack_start(self.runbox, expand=False, fill=False)
 
        self.scrollbox.pack_start(DroppableHSeparator(self.scrollbox), expand=False, fill=False)
 
        self.queuebox = QueuedBox(self)
        self.scrollbox.pack_start(self.queuebox, expand=False, fill=False)
 
        self.scrollbox.pack_start(SpacerBox(self), expand=True, fill=True) 
 
        self.mainscroll.add_with_viewport(self.scrollbox)
 
        self.paned.pack2(self.mainscroll, resize=True, shrink=False)
 
        self.box1.pack_start(self.paned)
 
        self.box1.show_all()
 
        self.mainwindow.add(self.box1)
 
        self.set_title()
        self.set_size()
 
        self.mainwindow.show()
 
        self.paned.set_position(0)
        self.search_field.grab_focus()
 
        self.updater = NewVersion.Updater(
            gtk_wrap, 
            self.new_version, 
            self.torrentqueue.start_new_torrent,
            self.confirm_install_new_version   ,
            self.global_error                  ,
            self.config['new_version']         ,
            self.config['current_version']     )
 
        self.nag()
 
        gtk.threads_leave()
 
    def window_event(self, widget, event, *args):
        if event.changed_mask == gtk.gdk.WINDOW_STATE_ICONIFIED:
            if self.config['minimize_to_tray']:
                if self.iconized == False:
                    self.mainwindow.hide()
            self.trayicon.set_toggle_state(self.iconized)
            self.iconized = not self.iconized
 
    def drag_leave(self, *args):
        self.drag_end()
 
    def make_new_torrent(self, widget=None):
        btspawn(self.torrentqueue, 'maketorrent')
 
    def accept_dropped_file(self, widget, context, x, y, selection,
                            targetType, time):
        if targetType == EXTERNAL_FILE_TYPE:
            d = selection.data.strip()
            file_uris = d.split('\r\n')
            for file_uri in file_uris:
                # this catches non-url entries, I've seen "\x00" at the end of lists
                if file_uri.find(':/') != -1:
                    file_name = zurllib.url2pathname(file_uri)
                    file_name = file_name[7:]
                    if os.name == 'nt':
                        file_name = file_name.strip('\\')
                    self.open_torrent( file_name )
        elif targetType == EXTERNAL_STRING_TYPE:
 
            data = selection.data.strip()
 
            # size must be > 0,0 for the intersection code to register it
            drop_rect = gtk.gdk.Rectangle(x, y, 1, 1)
            if ((self.search_field.intersect(drop_rect) is not None) and
                (not data.lower().endswith(".torrent"))):
 
                client_point = self.mainwindow.translate_coordinates(self.search_field, x, y)
                layout_offset = self.search_field.get_layout_offsets()
                point = []
                # subtract (not add) the offset, because we're hit-testing the layout, not the widget
                point.append(client_point[0] - layout_offset[0])
                point.append(client_point[1] - layout_offset[1])
                # ha ha ha. pango is so ridiculous
                point[0] *= pango.SCALE
                point[1] *= pango.SCALE
                layout = self.search_field.get_layout()
                position = layout.xy_to_index(*point)
                self.search_field.insert_text(data, position[0])
            else:                    
                self.open_url(data)
 
    def drag_highlight(self, widget=None):
        widgets = (self.knownbox, self.runbox, self.queuebox) 
        for w in widgets:
            if w != widget:
                w.drag_unhighlight()
        for w in widgets:
            if w == widget:
                w.drag_highlight()
                self.add_unhighlight_handle()
 
    def drag_end(self):
        self.drag_highlight(widget=None)
        self.mainscroll.stop_scrolling()
 
    def set_title(self, torrentName=None, fractionDone=None):
        title = app_name
        trunc = '...'
        sep = ': '
 
        if self.config['pause']:
            title += sep+_("(stopped)")
        elif len(self.running_torrents) == 1 and torrentName and \
               fractionDone is not None:
            maxlen = WINDOW_TITLE_LENGTH - len(app_name) - len(trunc) - len(sep)
            if len(torrentName) > maxlen:
                torrentName = torrentName[:maxlen] + trunc
            title = '%s%s%0.1f%%%s%s'% (app_name,
                                            sep,
                                            (int(fractionDone*1000)/10),
                                            sep,
                                            torrentName)
        elif len(self.running_torrents) > 1:
            title += sep+_("(multiple)")
 
        if self.mainwindow.get_title() != title:
            self.mainwindow.set_title(title)
        if self.trayicon.get_tooltip() != title:
            self.trayicon.set_tooltip(title)
 
    def _guess_size(self):
        paned_height = self.scrollbox.size_request()[1]
        if hasattr(self.paned, 'style_get_property'):
            paned_height += self.paned.style_get_property('handle-size')
        else:
            paned_height += 5
        paned_height += self.paned.get_position()
        paned_height += 4 # fudge factor, probably from scrolled window beveling ?
        paned_height = max(paned_height, MIN_MULTI_PANE_HEIGHT)
 
        new_height = self.menubar.size_request()[1] + \
                     self.box2.size_request()[1] + \
                     paned_height
        new_height = min(new_height, MAX_WINDOW_HEIGHT)
        new_width = max(self.scrollbox.size_request()[0] + SCROLLBAR_WIDTH, WINDOW_WIDTH)
        return new_width, new_height
 
    def set_size(self):
        if not self.custom_size:
            self.mainwindow.resize(*self._guess_size())
 
    def size_was_allocated(self, *args):
        current_size = self.mainwindow.get_size()
        target_size = self._guess_size()
        if current_size == target_size:
            self.set_custom_size(False)
        else:
            self.set_custom_size(True)
 
    def resize_to_fit(self):
        self.set_custom_size(False)
        self.set_size()
 
    def set_custom_size(self, val):
        self.custom_size = val
        # BUG this is a hack:
        self.viewmenu.get_submenu().get_children()[1].set_sensitive(val)
 
    # BUG need to add handler on resize event to keep track of
    # old_position when pane is hidden manually
    def split_pane(self):
        pos = self.paned.get_position()
        if pos > 0:
            self.paned.old_position = pos
            self.paned.set_position(0)
        else:
            if hasattr(self.paned, 'old_position'):
                self.paned.set_position(self.paned.old_position)
            else:
                self.maximize_known_pane()
 
    def maximize_known_pane(self):
        self.set_pane_position(self.knownbox.size_request()[1])        
 
    def set_pane_position(self, pane_position):
            pane_position = min(MAX_WINDOW_HEIGHT//2, pane_position)
            self.paned.set_position(pane_position)
 
    def toggle_known(self, widget=None):
        self.split_pane()
 
    def open_window(self, window_name, *args, **kwargs):
        if os.name == 'nt':
            self.mainwindow.present()
        savewidget = SaveFileSelection
        if window_name == 'savedir':
            savewidget = CreateFolderSelection
            window_name = 'savefile'
        if self.child_windows.has_key(window_name):
            if window_name == 'savefile':
                kwargs['show'] = False
                self.postponed_save_windows.append(savewidget(self, **kwargs))
            return
 
        if window_name == 'log'       :
            self.child_windows[window_name] = LogWindow(self, self.logbuffer, self.config)
        elif window_name == 'about'   :
            self.child_windows[window_name] = AboutWindow(self, lambda w: self.donate())
        elif window_name == 'help'    :
            self.child_windows[window_name] = HelpWindow(self, makeHelp('bittorrent', defaults))
        elif window_name == 'settings':
            self.child_windows[window_name] = SettingsWindow(self, self.config, self.set_config)
        elif window_name == 'version' :
            self.child_windows[window_name] = VersionWindow(self, *args)
        elif window_name == 'openfile':
            self.child_windows[window_name] = OpenFileSelection(self, **kwargs)
        elif window_name == 'savefile':
            self.child_windows[window_name] = savewidget(self, **kwargs)
        elif window_name == 'choosefolder':
            self.child_windows[window_name] = ChooseFolderSelection(self, **kwargs)            
        elif window_name == 'enterurl':
            self.child_windows[window_name] = EnterUrlDialog(self, **kwargs)
 
        return self.child_windows[window_name]
 
    def window_closed(self, window_name):
        if self.child_windows.has_key(window_name):
            del self.child_windows[window_name]
        if window_name == 'savefile' and self.postponed_save_windows:
            newwin = self.postponed_save_windows.pop(-1)
            newwin.show()
            self.child_windows['savefile'] = newwin
 
    def close_window(self, window_name):
        self.child_windows[window_name].close(None)
 
    def new_version(self, newversion, download_url):
        if not self.config['notified'] or \
               newversion != NewVersion.Version.from_str(self.config['notified']):
            if not self.torrents.has_key(self.updater.infohash):
                self.open_window('version', newversion, download_url)
            else:
                dlpath = os.path.split(self.torrents[self.updater.infohash].dlpath)[0]
                self.updater.set_installer_dir(dlpath)
                self.updater.start_install()
 
    def check_version(self):
        self.updater.check() 
 
    def start_auto_update(self):
        if not self.torrents.has_key(self.updater.infohash):
            self.updater.download()
        else:
            self.global_error(INFO, _("Already downloading %s installer") % self.updater.version)
 
    def confirm_install_new_version(self):
        MessageDialog(self.mainwindow,
                      _("Install new %s now?")%app_name,
                      _("Do you want to quit %s and install the new version, "
                        "%s, now?")%(app_name,self.updater.version),
                      type=gtk.MESSAGE_QUESTION,
                      buttons=gtk.BUTTONS_YES_NO,
                      yesfunc=self.install_new_version,
                      nofunc=None,
                      default=gtk.RESPONSE_YES
                      )
 
    def install_new_version(self):
        self.updater.launch_installer(self.torrentqueue)
        self.cancel()
 
 
    def open_help(self,widget):
        if self.helpwindow is None:
            msg = (_("%s help is at \n%s\nWould you like to go there now?")%
                  (app_name, HELP_URL))
            self.helpwindow = MessageDialog(self.mainwindow,
                                            _("Visit help web page?"),
                                            msg,
                                            type=gtk.MESSAGE_QUESTION,
                                            buttons=gtk.BUTTONS_OK_CANCEL,
                                            yesfunc=self.visit_help,
                                            nofunc =self.help_closed,
                                            default=gtk.RESPONSE_OK
                                            )
 
    def visit_help(self):
        self.visit_url(HELP_URL)
        self.help_closed()
 
    def close_help(self):
        self.helpwindow.close()
 
    def help_closed(self, widget=None):
        self.helpwindow = None
 
 
    def set_config(self, option, value):
        self.config[option] = value
        if option == 'display_interval':
            self.init_updates()
        self.torrentqueue.set_config(option, value)
 
 
    def confirm_remove_finished_torrents(self,widget):
        count = 0
        for infohash, t in self.torrents.iteritems():
            if t.state == KNOWN and t.completion >= 1:
                count += 1
        if count:
            if self.paned.get_position() == 0:
                self.toggle_known()
            msg = ''
            if count == 1:
                msg = _("There is one finished torrent in the list. ") + \
                      _("Do you want to remove it?")
            else:
                msg = _("There are %d finished torrents in the list. ") % count +\
                      _("Do you want to remove all of them?")
            MessageDialog(self.mainwindow,
                          _("Remove all finished torrents?"),
                          msg,
                          type=gtk.MESSAGE_QUESTION,
                          buttons=gtk.BUTTONS_OK_CANCEL,
                          yesfunc=self.remove_finished_torrents,
                          default=gtk.RESPONSE_OK)
        else:
            MessageDialog(self.mainwindow,
                          _("No finished torrents"),
                          _("There are no finished torrents to remove."),
                          type=gtk.MESSAGE_INFO,
                          default=gtk.RESPONSE_OK)
 
    def remove_finished_torrents(self):
        for infohash, t in self.torrents.iteritems():
            if t.state == KNOWN and t.completion >= 1:
                self.torrentqueue.remove_torrent(infohash)
        if self.paned.get_position() > 0:
            self.toggle_known()
 
    def cancel(self, widget=None):
        for window_name in self.child_windows.keys():
            self.close_window(window_name)
 
        if self.errordialog is not None:
            self.errordialog.destroy()
            self.errors_closed()
 
        for t in self.torrents.itervalues():
            if t.widget is not None:
                t.widget.close_child_windows()
 
        self.torrentqueue.set_done()
        gtk.main_quit()
 
    # Currently called if the user started bittorrent from a terminal
    # and presses ctrl-c there, or if the user quits BitTorrent from
    # the tray icon (on windows)   
    def quit(self):
        self.mainwindow.destroy()
 
    def make_statusrequest(self):
        if self.config['pause']:
            return True
        for infohash, t in self.running_torrents.iteritems():
            self.torrentqueue.request_status(infohash, t.widget.peerlistwindow
                             is not None, t.widget.filelistwindow is not None)
        if not len(self.running_torrents):
            self.status_light.send_message('empty')
        return True
 
    def enter_url_to_open(self, widget): 
        self.open_window('enterurl')
 
    def open_url(self, url):
        self.torrentqueue.start_new_torrent_by_name(url)
 
    def select_torrent_to_open(self, widget):
        open_location = self.config['open_from']
        if not open_location:
            open_location = self.config['save_in']
        path = smart_dir(open_location) 
        self.open_window('openfile',
                         title=_("Open torrent:"),
                         fullname=path,
                         got_location_func=self.open_torrent,
                         no_location_func=lambda: self.window_closed('openfile'))
 
 
    def open_torrent(self, name):
        self.window_closed('openfile')
        open_location = os.path.split(name)[0]
        if open_location[-1] != os.sep:
            open_location += os.sep
        self.set_config('open_from',  open_location)
 
        self.torrentqueue.start_new_torrent_by_name(name)
 
    def change_save_location(self, infohash):
        def no_location():
            self.window_closed('savefile')
 
        t = self.torrents[infohash]
        metainfo = t.metainfo
 
        selector = self.open_window(metainfo.is_batch and 'savedir' or \
                                                          'savefile',
                      title=_("Change save location for ") + metainfo.name,
                      fullname=t.dlpath,
                      got_location_func = \
                            lambda fn: self.got_changed_location(infohash, fn),
                      no_location_func=no_location)
 
    def got_changed_location(self, infohash, fullpath):
        self.window_closed('savefile')
        self.torrentqueue.set_save_location(infohash, fullpath)
 
    def save_location(self, infohash, metainfo):
        name = metainfo.name_fs
 
        if self.config['save_as'] and \
               os.access(os.path.split(self.config['save_as'])[0], os.W_OK):
            path = self.config['save_as']
            self.got_location(infohash, path, store_in_config=False)
            self.config['save_as'] = ''
            return
 
        path = smart_dir(self.config['save_in'])
 
        fullname = os.path.join(path, name)
 
        if not self.config['ask_for_save']:
            if os.access(fullname, os.F_OK):
                message = MessageDialog(self.mainwindow,
                                        _("File exists!"),
                                        _('"%s" already exists. '
                                          "Do you want to choose a different file name?") % path_wrap(name),
                                        buttons=gtk.BUTTONS_YES_NO,
                                        nofunc= lambda : self.got_location(infohash, fullname),
                                        yesfunc=lambda : self.get_save_location(infohash, metainfo, fullname),
                                        default=gtk.RESPONSE_NO)
 
            else:
                self.got_location(infohash, fullname)
        else:
            self.get_save_location(infohash, metainfo, fullname)
 
    def get_save_location(self, infohash, metainfo, fullname):
        def no_location():
            self.window_closed('savefile')
            self.torrentqueue.remove_torrent(infohash)
 
        selector = self.open_window(metainfo.is_batch and 'savedir' or \
                                                          'savefile',
                                    title=_("Save location for ") + metainfo.name,
                                    fullname=fullname,
                                    got_location_func = lambda fn: \
                                              self.got_location(infohash, fn),
                                    no_location_func=no_location)
 
        self.torrents[infohash].widget = selector
 
    def got_location(self, infohash, fullpath, store_in_config=True):
        self.window_closed('savefile')
        self.torrents[infohash].widget = None
        save_in = os.path.split(fullpath)[0]
 
        metainfo = self.torrents[infohash].metainfo
        if metainfo.is_batch:
            bottom_dirs, top_dir_name = os.path.split(save_in)
            if metainfo.name_fs == top_dir_name:
 
                message = MessageDialog(self.mainwindow, _("Directory exists!"),
                                        _('"%s" already exists.'\
                                          " Do you intend to create an identical,"\
                                          " duplicate directory inside the existing"\
                                          " directory?")%path_wrap(save_in),
                                        buttons=gtk.BUTTONS_YES_NO,
                                        nofunc =lambda : self.got_location(infohash, save_in ),
                                        yesfunc=lambda : self._got_location(infohash, save_in, fullpath, store_in_config=store_in_config),
                                        default=gtk.RESPONSE_NO,
                                        )
                return
        self._got_location(infohash, save_in, fullpath, store_in_config=store_in_config)
 
    def _got_location(self, infohash, save_in, fullpath, store_in_config=True):
        if store_in_config:
            if save_in[-1] != os.sep:
                save_in += os.sep
            self.set_config('save_in', save_in)
        self.torrents[infohash].dlpath = fullpath
        self.torrentqueue.set_save_location(infohash, fullpath)
 
    def add_unhighlight_handle(self):
        if self.unhighlight_handle is not None:
            gobject.source_remove(self.unhighlight_handle)
 
        self.unhighlight_handle = gobject.timeout_add(2000,
                                                      self.unhighlight_after_a_while,
                                                      priority=gobject.PRIORITY_LOW)
 
    def unhighlight_after_a_while(self):
        self.drag_highlight()
        gobject.source_remove(self.unhighlight_handle)
        self.unhighlight_handle = None
        return False
 
    def init_updates(self):
        if self.update_handle is not None:
            gobject.source_remove(self.update_handle)
        self.update_handle = gobject.timeout_add(
            int(self.config['display_interval'] * 1000),
            self.make_statusrequest)
 
    def remove_torrent_widget(self, infohash):
        t = self.torrents[infohash]
        self.lists[t.state].remove(infohash)
        if t.state == RUNNING:
            del self.running_torrents[infohash]
            self.set_title()
        if t.state == ASKING_LOCATION:
            if t.widget is not None:
                t.widget.destroy()
            return
 
        if t.state in (KNOWN, RUNNING, QUEUED):
            t.widget.close_child_windows()
 
        if t.state == RUNNING:
            self.runbox.remove(t.widget)
        elif t.state == QUEUED:
            self.queuebox.remove(t.widget)
        elif t.state == KNOWN:
            self.knownbox.remove(t.widget)
 
        t.widget.destroy()
 
        self.set_size()
 
    def create_torrent_widget(self, infohash, queuepos=None):
        t = self.torrents[infohash]
        l = self.lists.setdefault(t.state, [])
        if queuepos is None:
            l.append(infohash)
        else:
            l.insert(queuepos, infohash)
        if t.state == ASKING_LOCATION:
            self.save_location(infohash, t.metainfo)
            self.nag()
            return
        elif t.state == RUNNING:
            self.running_torrents[infohash] = t
            if not self.config['pause']:
                t.widget = RunningTorrentBox(infohash, t.metainfo, t.dlpath,
                                             t.completion, self)
            else:
                t.widget = PausedTorrentBox(infohash, t.metainfo, t.dlpath,
                                             t.completion, self)
            box = self.runbox
        elif t.state == QUEUED:
            t.widget = QueuedTorrentBox(infohash, t.metainfo, t.dlpath,
                                        t.completion, self)
            box = self.queuebox
        elif t.state == KNOWN:
            t.widget = KnownTorrentBox(infohash, t.metainfo, t.dlpath,
                                       t.completion, self)
            box = self.knownbox
        box.pack_start(t.widget, expand=False, fill=False)
        if queuepos is not None:
            box.reorder_child(t.widget, queuepos)
 
        self.set_size()
 
    def log_text(self, text, severity=ERROR):
        self.logbuffer.log_text(text, severity)
        if self.child_windows.has_key('log'):
            self.child_windows['log'].scroll_to_end()
 
    def _error(self, severity, err_str):
        err_str = err_str.decode('utf-8', 'replace').encode('utf-8')
        err_str = err_str.strip()
        if severity >= ERROR:
            self.error_modal(err_str)
        self.log_text(err_str, severity)
 
    def error(self, infohash, severity, text):
        if self.torrents.has_key(infohash):
            name = self.torrents[infohash].metainfo.name
            err_str = '"%s" : %s'%(name,text)
            self._error(severity, err_str)
        else:
            ihex = infohash.encode('hex')
            err_str = '"%s" : %s'%(ihex,text)
            self._error(severity, err_str)
            self._error(WARNING, 'Previous error raised for invalid infohash: "%s"' % ihex)
 
    def global_error(self, severity, text):
        err_str = _("(global message) : %s")%text
        self._error(severity, err_str)
 
    def error_modal(self, text):
        if self.child_windows.has_key('log'):
            return
 
        title = _("%s Error") % app_name
 
        if self.errordialog is not None:
            if not self.errordialog.multi:
                self.errordialog.destroy()
                self.errordialog = MessageDialog(self.mainwindow, title, 
                                                 _("Multiple errors have occurred. "
                                                   "Click OK to view the error log."),
                                                 buttons=gtk.BUTTONS_OK_CANCEL,
                                                 yesfunc=self.multiple_errors_yes,
                                                 nofunc=self.errors_closed,
                                                 default=gtk.RESPONSE_OK
                                                 )
                self.errordialog.multi = True
            else:
                # already showing the multi error dialog, so do nothing
                pass
        else:
            self.errordialog = MessageDialog(self.mainwindow, title, text,
                                             yesfunc=self.errors_closed,
                                             default=gtk.RESPONSE_OK)
            self.errordialog.multi = False
 
 
    def multiple_errors_yes(self):
        self.errors_closed()
        self.open_window('log')
 
    def errors_closed(self):
        self.errordialog = None
 
    def open_log(self):
        self.open_window('log')
 
    def stop_queue(self):
        self.set_config('pause', True)
        self.set_title()
        self.status_light.send_message('stop')
        self.set_seen_remote_connections(False)
        self.set_seen_connections(False)
        q = list(self.runbox.get_queue())
        for infohash in q:
            t = self.torrents[infohash]
            self.remove_torrent_widget(infohash)
            self.create_torrent_widget(infohash)
 
    def restart_queue(self):
        self.set_config('pause', False)
        q = list(self.runbox.get_queue())
        for infohash in q:
            t = self.torrents[infohash]
            self.remove_torrent_widget(infohash)
            self.create_torrent_widget(infohash)
        self.start_status_light()
 
    def start_status_light(self):
        if len(self.running_torrents):
            self.status_light.send_message('start')
        else:
            self.status_light.send_message('empty')
 
    def update_status(self, torrent, statistics):
        if self.config['pause']:
            self.status_light.send_message('start')
            return
 
        if self.seen_remote_connections:
            self.status_light.send_message('seen_remote_peers')
        elif self.seen_connections:
            self.status_light.send_message('seen_peers')
        else:
            self.start_status_light()
 
        self.running_torrents[torrent].widget.update_status(statistics)
        if statistics.get('numPeers'):
            self.set_seen_connections(seen=True)
        if (not self.seen_remote_connections and
            statistics.get('ever_got_incoming')):
            self.set_seen_remote_connections(seen=True)
        if self.updater is not None:
            updater_infohash = self.updater.infohash
            if self.torrents.has_key(updater_infohash):
                updater_torrent = self.torrents[updater_infohash]
                if updater_torrent.state == QUEUED:
                    self.change_torrent_state(updater_infohash, RUNNING,
                                              index=0, replaced=0,
                                              force_running=True)
 
    def set_seen_remote_connections(self, seen=False):
        if seen:
            self.status_light.send_message('seen_remote_peers')
        self.seen_remote_connections = seen
 
    def set_seen_connections(self, seen=False):
        if seen:
            self.status_light.send_message('seen_peers')
        self.seen_connections = seen
 
    def new_displayed_torrent(self, infohash, metainfo, dlpath, state, config,
                              completion=None, uptotal=0, downtotal=0):
        t = Struct()
        t.metainfo = metainfo
        t.dlpath = dlpath
        t.state = state
        t.config = config
        t.completion = completion
        t.uptotal = uptotal
        t.downtotal = downtotal
        t.widget = None
        self.torrents[infohash] = t
        self.create_torrent_widget(infohash)
 
    def torrent_state_changed(self, infohash, dlpath, state, completion,
                              uptotal, downtotal, queuepos=None):
        t = self.torrents[infohash]
        self.remove_torrent_widget(infohash)
        t.dlpath = dlpath
        t.state = state
        t.completion = completion
        t.uptotal = uptotal
        t.downtotal = downtotal
        self.create_torrent_widget(infohash, queuepos)
 
    def reorder_torrent(self, infohash, queuepos):
        self.remove_torrent_widget(infohash)
        self.create_torrent_widget(infohash, queuepos)
 
    def update_completion(self, infohash, completion, files_left=None,
                          files_allocated=None):
        t = self.torrents[infohash]
        if files_left is not None and t.widget.filelistwindow is not None:
            t.widget.filelistwindow.update(files_left, files_allocated)
 
    def removed_torrent(self, infohash):
        self.remove_torrent_widget(infohash)
        del self.torrents[infohash]
 
    def change_torrent_state(self, infohash, newstate, index=None,
                             replaced=None, force_running=False):
        t = self.torrents[infohash]
        pred = succ = None
        if index is not None:
            l = self.lists.setdefault(newstate, [])
            if index > 0:
                pred = l[index - 1]
            if index < len(l):
                succ = l[index]
        self.torrentqueue.change_torrent_state(infohash, t.state, newstate,
                                         pred, succ, replaced, force_running)
 
    def finish(self, infohash):
        t = self.torrents[infohash]
        if t is None or t.state == KNOWN:
            return
        self.change_torrent_state(infohash, KNOWN)
 
    def confirm_replace_running_torrent(self, infohash, replaced, index):
        replace_func = lambda *args: self.change_torrent_state(infohash,
                                RUNNING, index, replaced)
        add_func     = lambda *args: self.change_torrent_state(infohash,
                                RUNNING, index, force_running=True)
        moved_torrent = self.torrents[infohash]
 
        if moved_torrent.state == RUNNING:
            self.change_torrent_state(infohash, RUNNING, index)
            return
 
        if self.config['start_torrent_behavior'] == 'replace':
            replace_func()
            return
        elif self.config['start_torrent_behavior'] == 'add':
            add_func()
            return
 
        moved_torrent_name = moved_torrent.metainfo.name
        confirm = MessageDialog(self.mainwindow,
                                _("Stop running torrent?"),
                                _('You are about to start "%s". Do you want to stop another running torrent as well?')%(moved_torrent_name),
                                type=gtk.MESSAGE_QUESTION,
                                buttons=gtk.BUTTONS_YES_NO,
                                yesfunc=replace_func,
                                nofunc=add_func,
                                default=gtk.RESPONSE_YES)
 
    def nag(self):
        if ((self.config['donated'] != version) and
            #(random.random() * NAG_FREQUENCY) < 1) and
            False):
            title = _("Have you donated?")
            message = _("Welcome to the new version of %s. Have you donated?")%app_name
            self.nagwindow = MessageDialog(self.mainwindow,
                                           title,
                                           message,
                                           type=gtk.MESSAGE_QUESTION,
                                           buttons=gtk.BUTTONS_YES_NO,
                                           yesfunc=self.nag_yes, nofunc=self.nag_no,
                                           default=gtk.RESPONSE_NO)
 
    def nag_no(self):
        self.donate()
 
    def nag_yes(self):
        self.set_config('donated', version)
        MessageDialog(self.mainwindow,
                      _("Thanks!"),
                      _("Thanks for donating! To donate again, "
                        'select "Donate" from the "Help" menu.'),
                      type=gtk.MESSAGE_INFO,
                      default=gtk.RESPONSE_OK
                      )
 
    def donate(self):
        self.visit_url(DONATE_URL)
 
 
    def visit_url(self, url, callback=None):
        t = threading.Thread(target=self._visit_url,
                             args=(url,callback))
        t.setDaemon(True)
        t.start()
 
    def _visit_url(self, url, callback=None):
        webbrowser.open(url)
        if callback:
            gtk_wrap(callback)
 
    def toggle_shown(self):
        if self.config['minimize_to_tray']:
            if self.mainwindow.get_property('visible'):
                self.mainwindow.hide()
            else:
                self.mainwindow.show_all()
        else:
            if not self.iconized:
                self.mainwindow.iconify()
            else:
                self.mainwindow.deiconify()
 
 
    def raiseerror(self, *args):
        raise ValueError('test traceback behavior')
 
#this class provides a thin layer around the loop so that the main window
#doesn't have to run it. It protects againstexceptions in mainwindow creation
#preventing the loop from starting (and causing "The grey screen of BT")
class MainLoop:
    def __init__(self):
        self.mainwindow = None
        self.started = 0
 
        gtk.threads_init()
 
    def set_mainwindow(self, mainwindow):
        self.mainwindow = mainwindow
 
    def run(self):
        self.mainwindow.traythread.start()
        gtk.threads_enter()        
 
        if self.mainwindow:
            self.mainwindow.ssbutton.set_paused(self.mainwindow.config['pause'])
            self.mainwindow.rate_slider_box.start()
            self.mainwindow.init_updates()
 
        try:
            #the main loop has been started
            self.started = 1
            gtk.main() 
        except KeyboardInterrupt:
            gtk.threads_leave()
            if self.mainwindow:
                self.mainwindow.torrentqueue.set_done()
            raise
 
        gtk.threads_leave()
 
    def quit(self):
        if self.mainwindow: 
            self.mainwindow.quit()
 
 
def btgui_exit_gtk(mainloop):
    # if the main loop has never run, we have to run it to flush blocking threads
    # if it has run, running it a second time will cause duplicate-destruction problems
    if not mainloop.started:
        # queue up a command to close the gui
        gobject.idle_add(lock_wrap, mainloop.quit)
        # run the main loop so we process all queued commands, then quit
        mainloop.run()
 
if __name__ == '__main__':
 
    mainloop = MainLoop()
 
    # make sure we start the gtk loop once before we close
    atexit.register(btgui_exit_gtk, mainloop)
 
    torrentqueue = TorrentQueue.TorrentQueue(config, ui_options, ipc)
    d = DownloadInfoFrame(config,TorrentQueue.ThreadWrappedQueue(torrentqueue))
 
    mainloop.set_mainwindow(d)
    global_log_func.logger = d.global_error
 
    startflag = threading.Event()
    dlthread = threading.Thread(target = torrentqueue.run,
                                args = (d, gtk_wrap, startflag))
    dlthread.setDaemon(False)
    dlthread.start()
    startflag.wait()
    # the wait may have been terminated because of an error
    if torrentqueue.initialized == -1:
        raise BTFailure(_("Could not start the TorrentQueue, see above for errors."))
 
    torrentqueue.rawserver.install_sigint_handler()
    for name in newtorrents:
        d.torrentqueue.start_new_torrent_by_name(name)
 
    try:
        mainloop.run()
    except KeyboardInterrupt:
        # the gtk main loop is closed in MainLoop
        sys.exit(1)
    d.trayicon.disable()