diff options
Diffstat (limited to 'BitTorrent/GUI.py')
-rw-r--r-- | BitTorrent/GUI.py | 679 |
1 files changed, 679 insertions, 0 deletions
diff --git a/BitTorrent/GUI.py b/BitTorrent/GUI.py new file mode 100644 index 0000000..529811f --- /dev/null +++ b/BitTorrent/GUI.py @@ -0,0 +1,679 @@ +# 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 Matt Chisholm + +from __future__ import division + +import gtk +import pango +import gobject +import os +import threading + +assert gtk.gtk_version >= (2, 6), _( "GTK %s or newer required") % '2.6' +assert gtk.pygtk_version >= (2, 6), _("PyGTK %s or newer required") % '2.6' + +from BitTorrent import app_name, FAQ_URL, languages, language_names +from BitTorrent.platform import image_root, read_language_file, write_language_file + +def lock_wrap(function, *args): + gtk.threads_enter() + function(*args) + gtk.threads_leave() + +def gtk_wrap(function, *args): + gobject.idle_add(lock_wrap, function, *args) + +SPACING = 8 +WINDOW_TITLE_LENGTH = 128 # do we need this? +WINDOW_WIDTH = 600 + +# get screen size from GTK +d = gtk.gdk.display_get_default() +s = d.get_default_screen() +MAX_WINDOW_HEIGHT = s.get_height() +MAX_WINDOW_WIDTH = s.get_width() +if os.name == 'nt': + MAX_WINDOW_HEIGHT -= 32 # leave room for start bar (exact) + MAX_WINDOW_HEIGHT -= 32 # and window decorations (depends on windows theme) +else: + MAX_WINDOW_HEIGHT -= 32 # leave room for window decorations (could be any size) + + +MIN_MULTI_PANE_HEIGHT = 107 + +BT_TARGET_TYPE = 0 +EXTERNAL_FILE_TYPE = 1 +EXTERNAL_STRING_TYPE = 2 + +BT_TARGET = ("application/x-bittorrent" , gtk.TARGET_SAME_APP, BT_TARGET_TYPE ) +EXTERNAL_FILE = ("text/uri-list" , 0 , EXTERNAL_FILE_TYPE ) + +#gtk(gdk actually) is totally unable to receive text drags +#of any sort in windows because they're too lazy to use OLE. +#this list is all the atoms I could possibly find so that +#url dnd works on linux from any browser. +EXTERNAL_TEXTPLAIN = ("text/plain" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_TEXT = ("TEXT" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_COMPOUND_TEXT = ("COMPOUND_TEXT" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_MOZILLA = ("text/x-moz-url" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_NETSCAPE = ("_NETSCAPE_URL" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_HTML = ("text/html" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_UNICODE = ("text/unicode" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_UTF8 = ("text/plain;charset=utf-8" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_UTF8_STRING = ("UTF8_STRING" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_STRING = ("STRING" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_OLE2_DND = ("OLE2_DND" , 0 , EXTERNAL_STRING_TYPE) +EXTERNAL_RTF = ("Rich Text Format" , 0 , EXTERNAL_STRING_TYPE) +#there should alse be text/plain;charset={current charset} + +TARGET_EXTERNAL = [EXTERNAL_FILE, + EXTERNAL_TEXTPLAIN, + EXTERNAL_TEXT, + EXTERNAL_COMPOUND_TEXT, + EXTERNAL_MOZILLA, + EXTERNAL_NETSCAPE, + EXTERNAL_HTML, + EXTERNAL_UNICODE, + EXTERNAL_UTF8, + EXTERNAL_UTF8_STRING, + EXTERNAL_STRING, + EXTERNAL_OLE2_DND, + EXTERNAL_RTF] + +TARGET_ALL = [BT_TARGET, + EXTERNAL_FILE, + EXTERNAL_TEXTPLAIN, + EXTERNAL_TEXT, + EXTERNAL_COMPOUND_TEXT, + EXTERNAL_MOZILLA, + EXTERNAL_NETSCAPE, + EXTERNAL_HTML, + EXTERNAL_UNICODE, + EXTERNAL_UTF8, + EXTERNAL_UTF8_STRING, + EXTERNAL_STRING, + EXTERNAL_OLE2_DND, + EXTERNAL_RTF] + +# a slightly hackish but very reliable way to get OS scrollbar width +sw = gtk.ScrolledWindow() +SCROLLBAR_WIDTH = sw.size_request()[0] - 48 +del sw + +def align(obj,x,y): + if type(obj) is gtk.Label: + obj.set_alignment(x,y) + return obj + else: + a = gtk.Alignment(x,y,0,0) + a.add(obj) + return a + +def halign(obj, amt): + return align(obj,amt,0.5) + +def lalign(obj): + return halign(obj,0) + +def ralign(obj): + return halign(obj,1) + +def valign(obj, amt): + return align(obj,0.5,amt) + +def malign(obj): + return valign(obj, 0.5) + +factory = gtk.IconFactory() + +# these don't seem to be documented anywhere: +# ICON_SIZE_BUTTON = 20x20 +# ICON_SIZE_LARGE_TOOLBAR = 24x24 + +for n in 'abort broken finished info pause paused play queued running remove status-running status-starting status-pre-natted status-natted status-stopped status-broken'.split(): + fn = os.path.join(image_root, 'icons', 'default', ('%s.png'%n)) + if os.access(fn, os.F_OK): + pixbuf = gtk.gdk.pixbuf_new_from_file(fn) + set = gtk.IconSet(pixbuf) + factory.add('bt-%s'%n, set) + # maybe we should load a default icon if none exists + +factory.add_default() + +def get_logo(size=32): + fn = os.path.join(image_root, 'logo', 'bittorrent_%d.png'%size) + logo = gtk.Image() + logo.set_from_file(fn) + return logo + +class Size(long): + """displays size in human-readable format""" + __slots__ = [] + size_labels = ['','K','M','G','T','P','E','Z','Y'] + radix = 2**10 + def __new__(cls, value): + self = long.__new__(cls, value) + return self + + def __init__(self, value): + long.__init__(self, value) + + def __str__(self, precision=None): + if precision is None: + precision = 0 + value = self + for unitname in self.size_labels: + if value < self.radix and precision < self.radix: + break + value /= self.radix + precision /= self.radix + if unitname and value < 10 and precision < 1: + return '%.1f %sB' % (value, unitname) + else: + return '%.0f %sB' % (value, unitname) + + +class Rate(Size): + """displays rate in human-readable format""" + __slots__ = [] + def __init__(self, value): + Size.__init__(self, value) + + def __str__(self, precision=2**10): + return '%s/s'% Size.__str__(self, precision=precision) + + +class Duration(float): + """displays duration in human-readable format""" + __slots__ = [] + def __str__(self): + if self > 365 * 24 * 60 * 60: + return '?' + elif self >= 172800: + return _("%d days") % (self//86400) # 2 days or longer + elif self >= 86400: + return _("1 day %d hours") % ((self-86400)//3600) # 1-2 days + elif self >= 3600: + return _("%d:%02d hours") % (self//3600, (self%3600)//60) # 1 h - 1 day + elif self >= 60: + return _("%d:%02d minutes") % (self//60, self%60) # 1 minute to 1 hour + elif self >= 0: + return _("%d seconds") % int(self) + else: + return _("0 seconds") + + +class FancyLabel(gtk.Label): + def __init__(self, label_string, *values): + self.label_string = label_string + gtk.Label.__init__(self, label_string%values) + + def set_value(self, *values): + self.set_text(self.label_string%values) + + +class IconButton(gtk.Button): + def __init__(self, label, iconpath=None, stock=None): + gtk.Button.__init__(self, label) + + self.icon = gtk.Image() + if stock is not None: + self.icon.set_from_stock(stock, gtk.ICON_SIZE_BUTTON) + elif iconpath is not None: + self.icon.set_from_file(iconpath) + else: + raise TypeError, 'IconButton needs iconpath or stock' + self.set_image(self.icon) + + +class LanguageChooser(gtk.Frame): + def __init__(self): + gtk.Frame.__init__(self, "Translate %s into:" % app_name) + self.set_border_width(SPACING) + + model = gtk.ListStore(*[gobject.TYPE_STRING] * 2) + default = model.append(("System default", '')) + + lang = read_language_file() + for l in languages: + it = model.append((language_names[l].encode('utf8'), l)) + if l == lang: + default = it + + self.combo = gtk.ComboBox(model) + cell = gtk.CellRendererText() + self.combo.pack_start(cell, True) + self.combo.add_attribute(cell, 'text', 0) + + if default is not None: + self.combo.set_active_iter(default) + + self.combo.connect('changed', self.changed) + box = gtk.VBox(spacing=SPACING) + box.set_border_width(SPACING) + box.pack_start(self.combo, expand=False, fill=False) + l = gtk.Label("You must restart %s for the\nlanguage " + "setting to take effect." % app_name) + l.set_alignment(0,1) + l.set_line_wrap(True) + box.pack_start(l, expand=False, fill=False) + self.add(box) + + def changed(self, *a): + it = self.combo.get_active_iter() + model = self.combo.get_model() + code = model.get(it, 1)[0] + write_language_file(code) + +class IconMixin(object): + def __init__(self): + iconname = os.path.join(image_root,'bittorrent.ico') + icon16 = gtk.gdk.pixbuf_new_from_file_at_size(iconname, 16, 16) + icon32 = gtk.gdk.pixbuf_new_from_file_at_size(iconname, 32, 32) + self.set_icon_list(icon16, icon32) + +class Window(IconMixin, gtk.Window): + def __init__(self, *args): + gtk.Window.__init__(self, *args) + IconMixin.__init__(self) + +class HelpWindow(Window): + def __init__(self, main, helptext): + Window.__init__(self) + self.set_title(_("%s Help")%app_name) + self.main = main + self.set_border_width(SPACING) + + self.vbox = gtk.VBox(spacing=SPACING) + + self.faq_box = gtk.HBox(spacing=SPACING) + self.faq_box.pack_start(gtk.Label(_("Frequently Asked Questions:")), expand=False, fill=False) + self.faq_url = gtk.Entry() + self.faq_url.set_text(FAQ_URL) + self.faq_url.set_editable(False) + self.faq_box.pack_start(self.faq_url, expand=True, fill=True) + self.faq_button = gtk.Button(_("Go")) + self.faq_button.connect('clicked', lambda w: self.main.visit_url(FAQ_URL) ) + self.faq_box.pack_start(self.faq_button, expand=False, fill=False) + self.vbox.pack_start(self.faq_box, expand=False, fill=False) + + self.cmdline_args = gtk.Label(helptext) + + self.cmdline_sw = ScrolledWindow() + self.cmdline_sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) + self.cmdline_sw.add_with_viewport(self.cmdline_args) + + self.cmdline_sw.set_size_request(self.cmdline_args.size_request()[0]+SCROLLBAR_WIDTH, 200) + + self.vbox.pack_start(self.cmdline_sw) + + self.add(self.vbox) + + self.show_all() + + if self.main is not None: + self.connect('destroy', lambda w: self.main.window_closed('help')) + else: + self.connect('destroy', lambda w: gtk.main_quit()) + gtk.main() + + + + def close(self, widget=None): + self.destroy() + + +class ScrolledWindow(gtk.ScrolledWindow): + def scroll_to_bottom(self): + child_height = self.child.child.size_request()[1] + self.scroll_to(0, child_height) + + def scroll_by(self, dx=0, dy=0): + v = self.get_vadjustment() + new_y = min(v.upper, v.value + dy) + self.scroll_to(0, new_y) + + def scroll_to(self, x=0, y=0): + v = self.get_vadjustment() + child_height = self.child.child.size_request()[1] + new_adj = gtk.Adjustment(y, 0, child_height) + self.set_vadjustment(new_adj) + + +class AutoScrollingWindow(ScrolledWindow): + def __init__(self): + ScrolledWindow.__init__(self) + self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | + gtk.DEST_DEFAULT_DROP, + TARGET_ALL, + gtk.gdk.ACTION_MOVE|gtk.gdk.ACTION_COPY) + self.connect('drag_motion' , self.drag_motion ) +# self.connect('drag_data_received', self.drag_data_received) + self.vscrolltimeout = None + +# def drag_data_received(self, widget, context, x, y, selection, targetType, time): +# print _("AutoScrollingWindow.drag_data_received("), widget + + def drag_motion(self, widget, context, x, y, time): + v = self.get_vadjustment() + if v.page_size - y <= 10: + amount = (10 - int(v.page_size - y)) * 2 + self.start_scrolling(amount) + elif y <= 10: + amount = (y - 10) * 2 + self.start_scrolling(amount) + else: + self.stop_scrolling() + return True + + def scroll_and_wait(self, amount, lock_held): + if not lock_held: + gtk.threads_enter() + self.scroll_by(0, amount) + if not lock_held: + gtk.threads_leave() + if self.vscrolltimeout is not None: + gobject.source_remove(self.vscrolltimeout) + self.vscrolltimeout = gobject.timeout_add(100, self.scroll_and_wait, amount, False) + #print "adding timeout", self.vscrolltimeout, amount + + def start_scrolling(self, amount): + if self.vscrolltimeout is not None: + gobject.source_remove(self.vscrolltimeout) + self.scroll_and_wait(amount, True) + + def stop_scrolling(self): + if self.vscrolltimeout is not None: + #print "removing timeout", self.vscrolltimeout + gobject.source_remove(self.vscrolltimeout) + self.vscrolltimeout = None + +class MessageDialog(IconMixin, gtk.MessageDialog): + flags = gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT + + def __init__(self, parent, title, message, + type=gtk.MESSAGE_ERROR, + buttons=gtk.BUTTONS_OK, + yesfunc=None, nofunc=None, + default=gtk.RESPONSE_OK + ): + gtk.MessageDialog.__init__(self, parent, + self.flags, + type, buttons, message) + IconMixin.__init__(self) + + self.set_size_request(-1, -1) + self.set_resizable(False) + self.set_title(title) + if default is not None: + self.set_default_response(default) + + self.label.set_line_wrap(True) + + self.connect('response', self.callback) + + self.yesfunc = yesfunc + self.nofunc = nofunc + if os.name == 'nt': + parent.present() + self.show_all() + + def callback(self, widget, response_id, *args): + if ((response_id == gtk.RESPONSE_OK or + response_id == gtk.RESPONSE_YES) and + self.yesfunc is not None): + self.yesfunc() + if ((response_id == gtk.RESPONSE_CANCEL or + response_id == gtk.RESPONSE_NO ) + and self.nofunc is not None): + self.nofunc() + self.destroy() + +class ErrorMessageDialog(MessageDialog): + flags = gtk.DIALOG_DESTROY_WITH_PARENT + + +class FileSelection(IconMixin, gtk.FileChooserDialog): + + def __init__(self, action, main, title='', fullname='', + got_location_func=None, no_location_func=None, + got_multiple_location_func=None, show=True): + gtk.FileChooserDialog.__init__(self, action=action, title=title, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK)) + IconMixin.__init__(self) + from BitTorrent.ConvertedMetainfo import filesystem_encoding + self.fsenc = filesystem_encoding + try: + fullname.decode('utf8') + except: + fullname = fullname.decode(self.fsenc) + self.set_default_response(gtk.RESPONSE_OK) + if action == gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER: + self.convert_button_box = gtk.HBox() + self.convert_button = gtk.Button(_("Choose an existing folder...")) + self.convert_button.connect('clicked', self.change_action) + self.convert_button_box.pack_end(self.convert_button, + expand=False, + fill=False) + self.convert_button_box.show_all() + self.set_extra_widget(self.convert_button_box) + elif action == gtk.FILE_CHOOSER_ACTION_OPEN: + self.all_filter = gtk.FileFilter() + self.all_filter.add_pattern('*') + self.all_filter.set_name(_("All Files")) + self.add_filter(self.all_filter) + self.torrent_filter = gtk.FileFilter() + self.torrent_filter.add_pattern('*.torrent') + self.torrent_filter.add_mime_type('application/x-bittorrent') + self.torrent_filter.set_name(_("Torrents")) + self.add_filter(self.torrent_filter) + self.set_filter(self.torrent_filter) + + self.main = main + self.set_modal(True) + self.set_destroy_with_parent(True) + if fullname: + if action == gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER: + self.set_filename(fullname) + elif action == gtk.FILE_CHOOSER_ACTION_OPEN: + if fullname[-1] != os.sep: + fullname = fullname + os.sep + path, filename = os.path.split(fullname) + self.set_current_folder(path) + else: + if fullname[-1] == os.sep: + fullname = fullname[:-1] + path, filename = os.path.split(fullname) + if gtk.gtk_version < (2,8): + path = path.encode(self.fsenc) + self.set_current_folder(path) + self.set_current_name(filename) + if got_multiple_location_func is not None: + self.got_multiple_location_func = got_multiple_location_func + self.set_select_multiple(True) + self.got_location_func = got_location_func + self.no_location_func = no_location_func + self.connect('response', self.got_response) + self.d_handle = self.connect('destroy', self.got_response, + gtk.RESPONSE_CANCEL) + if show: + self.show() + + def change_action(self, widget): + if self.get_action() == gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER: + self.convert_button.set_label(_("Create a new folder...")) + self.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + elif self.get_action() == gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER: + self.convert_button.set_label(_("Choose an existing folder...")) + self.set_action(gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER) + + def got_response(self, widget, response): + if response == gtk.RESPONSE_OK: + if self.get_select_multiple(): + if self.got_multiple_location_func is not None: + self.got_multiple_location_func(self.get_filenames()) + elif self.got_location_func is not None: + fn = self.get_filename() + if fn: + self.got_location_func(fn) + else: + self.no_location_func() + else: + if self.no_location_func is not None: + self.no_location_func() + self.disconnect(self.d_handle) + self.destroy() + + def done(self, widget=None): + if self.get_select_multiple(): + self.got_multiple_location() + else: + self.got_location() + self.disconnect(self.d_handle) + self.destroy() + + def close_child_windows(self): + self.destroy() + + def close(self, widget=None): + self.destroy() + + +class OpenFileSelection(FileSelection): + + def __init__(self, *args, **kwargs): + FileSelection.__init__(self, gtk.FILE_CHOOSER_ACTION_OPEN, *args, + **kwargs) + + +class SaveFileSelection(FileSelection): + + def __init__(self, *args, **kwargs): + FileSelection.__init__(self, gtk.FILE_CHOOSER_ACTION_SAVE, *args, + **kwargs) + + +class ChooseFolderSelection(FileSelection): + + def __init__(self, *args, **kwargs): + FileSelection.__init__(self, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + *args, **kwargs) + +class CreateFolderSelection(FileSelection): + + def __init__(self, *args, **kwargs): + FileSelection.__init__(self, gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER, + *args, **kwargs) + + +class FileOrFolderSelection(FileSelection): + def __init__(self, *args, **kwargs): + FileSelection.__init__(self, gtk.FILE_CHOOSER_ACTION_OPEN, *args, + **kwargs) + self.select_file = _("Select a file" ) + self.select_folder = _("Select a folder") + self.convert_button_box = gtk.HBox() + self.convert_button = gtk.Button(self.select_folder) + self.convert_button.connect('clicked', self.change_action) + self.convert_button_box.pack_end(self.convert_button, + expand=False, + fill=False) + self.convert_button_box.show_all() + self.set_extra_widget(self.convert_button_box) + self.reset_by_action() + self.set_filter(self.all_filter) + + + def change_action(self, widget): + if self.get_action() == gtk.FILE_CHOOSER_ACTION_OPEN: + self.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + elif self.get_action() == gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER: + self.set_action(gtk.FILE_CHOOSER_ACTION_OPEN) + self.reset_by_action() + + def reset_by_action(self): + if self.get_action() == gtk.FILE_CHOOSER_ACTION_OPEN: + self.convert_button.set_label(self.select_folder) + self.set_title(self.select_file) + elif self.get_action() == gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER: + self.convert_button.set_label(self.select_file) + self.set_title(self.select_folder) + + def set_title(self, title): + mytitle = title + ':' + FileSelection.set_title(self, mytitle) + + +class PaddedHSeparator(gtk.VBox): + def __init__(self, spacing=SPACING): + gtk.VBox.__init__(self) + self.sep = gtk.HSeparator() + self.pack_start(self.sep, expand=False, fill=False, padding=spacing) + self.show_all() + + +class HSeparatedBox(gtk.VBox): + + def new_separator(self): + return PaddedHSeparator() + + def _get_children(self): + return gtk.VBox.get_children(self) + + def get_children(self): + return self._get_children()[0::2] + + def _reorder_child(self, child, index): + gtk.VBox.reorder_child(self, child, index) + + def reorder_child(self, child, index): + children = self._get_children() + oldindex = children.index(child) + sep = None + if oldindex == len(children) - 1: + sep = children[oldindex-1] + else: + sep = children[oldindex+1] + + newindex = index*2 + if newindex == len(children) -1: + self._reorder_child(sep, newindex-1) + self._reorder_child(child, newindex) + else: + self._reorder_child(child, newindex) + self._reorder_child(sep, newindex+1) + + def pack_start(self, widget, *args, **kwargs): + if len(self._get_children()): + s = self.new_separator() + gtk.VBox.pack_start(self, s, *args, **kwargs) + s.show() + gtk.VBox.pack_start(self, widget, *args, **kwargs) + + def pack_end(self, widget, *args, **kwargs): + if len(self._get_children()): + s = self.new_separator() + gtk.VBox.pack_start(self, s, *args, **kwargs) + s.show() + gtk.VBox.pack_end(self, widget, *args, **kwargs) + + def remove(self, widget): + children = self._get_children() + if len(children) > 1: + index = children.index(widget) + if index == 0: + sep = children[index+1] + else: + sep = children[index-1] + sep.destroy() + gtk.VBox.remove(self, widget) |