summaryrefslogtreecommitdiffstats
path: root/bittorrent.py
diff options
context:
space:
mode:
Diffstat (limited to 'bittorrent.py')
-rwxr-xr-xbittorrent.py3834
1 files changed, 3834 insertions, 0 deletions
diff --git a/bittorrent.py b/bittorrent.py
new file mode 100755
index 0000000..548f922
--- /dev/null
+++ b/bittorrent.py
@@ -0,0 +1,3834 @@
+#!/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()
+