Account Management Implemented.

Account management is now implemented for the GTK UI. Some changes to the core were required since the clients need to know which authentication levels exist, and, to expose account creation, update, and removal to the clients. The best effort is done to try not to compromise the auth file.
This commit is contained in:
Pedro Algarvio 2011-04-24 17:38:35 +01:00
parent 6ed3136c8e
commit 43e3fe2a1a
8 changed files with 661 additions and 182 deletions

View File

@ -2,6 +2,7 @@
# authmanager.py
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
@ -40,7 +41,7 @@ import logging
import deluge.component as component
import deluge.configmanager as configmanager
from deluge.error import BadLoginError, AuthenticationRequired
from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError
log = logging.getLogger(__name__)
@ -51,21 +52,35 @@ AUTH_LEVEL_ADMIN = 10
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
AUTH_LEVELS_MAPPING = {
'NONE': AUTH_LEVEL_NONE,
'READONLY': AUTH_LEVEL_READONLY,
'DEFAULT': AUTH_LEVEL_NORMAL,
'NORMAL': AUTH_LEVEL_DEFAULT,
'ADMIN': AUTH_LEVEL_ADMIN
}
AUTH_LEVELS_MAPPING_REVERSE = {}
for key, value in AUTH_LEVELS_MAPPING.iteritems():
AUTH_LEVELS_MAPPING_REVERSE[value] = key
class Account(object):
__slots__ = ('username', 'password', 'auth_level')
def __init__(self, username, password, auth_level):
__slots__ = ('username', 'password', 'authlevel')
def __init__(self, username, password, authlevel):
self.username = username
self.password = password
self.auth_level = auth_level
self.authlevel = authlevel
def data(self, include_private=True):
rv = self.__dict__.copy()
if not include_private:
rv['password'] = ''
return rv
def data(self):
return {
'username': self.username,
'password': self.password,
'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel],
'authlevel_int': self.authlevel
}
def __repr__(self):
return ('<Account username="%(username)s" auth_level=%(auth_level)s>' %
return ('<Account username="%(username)s" authlevel=%(authlevel)s>' %
self.__dict__)
@ -104,52 +119,71 @@ class AuthManager(component.Component):
"Username and Password are required.", username
)
self.__test_existing_account(username)
if username not in self.__auth:
# Let's try to re-load the file.. Maybe it's been updated
self.__load_auth_file()
if username not in self.__auth:
raise BadLoginError("Username does not exist", username)
if self.__auth[username].password == password:
# Return the users auth level
return self.__auth[username].auth_level
return self.__auth[username].authlevel
elif not password and self.__auth[username].password:
raise AuthenticationRequired("Password is required", username)
else:
raise BadLoginError("Password does not match")
raise BadLoginError("Password does not match", username)
def get_known_accounts(self, include_private_data=False):
def get_known_accounts(self):
"""
Returns a list of known deluge usernames.
"""
self.__load_auth_file()
rv = {}
for account in self.__auth.items():
rv[account.username] = account.data(include_private_data)
return rv
return [account.data() for account in self.__auth.values()]
def create_account(self, username, password='', auth_level=AUTH_LEVEL_DEFAULT):
def create_account(self, username, password, authlevel):
if username in self.__auth:
raise Something()
self.__create_account(username, password, auth_level)
raise AuthManagerError("Username in use.", username)
try:
self.__auth[username] = Account(username, password,
AUTH_LEVELS_MAPPING[authlevel])
self.write_auth_file()
return True
except Exception, err:
log.exception(err)
raise err
def update_account(self, username, password='', auth_level=AUTH_LEVEL_DEFAULT):
if username in self.__auth:
raise Something()
self.__create_account(username, password, auth_level)
def update_account(self, username, password, authlevel):
if username not in self.__auth:
raise AuthManagerError("Username not known", username)
try:
self.__auth[username].username = username
self.__auth[username].password = password
self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel]
self.write_auth_file()
return True
except Exception, err:
log.exception(err)
raise err
def remove_account(self, username):
if username in self.__auth:
raise Something()
if username not in self.__auth:
raise AuthManagerError("Username not known", username)
elif username == component.get("RPCServer").get_session_user():
raise AuthManagerError(
"You cannot delete your own account while logged in!", username
)
del self.__auth[username]
self.write_auth_file()
if component.get("RPCServer").get_session_user() == username:
# Force a client logout by the server
component.get("RPCServer").logout_current_session()
return True
def write_auth_file(self):
old_auth_file = configmanager.get_config_dir("auth")
new_auth_file = old_auth_file + '.new'
fd = open(new_auth_file, "w")
for account in self.__auth.items():
for account in self.__auth.values():
fd.write(
"%(username)s:%(password)s:%(auth_level)s\n" % account.__dict__
"%(username)s:%(password)s:%(authlevel_int)s\n" % account.data()
)
fd.flush()
os.fsync(fd.fileno())
@ -157,18 +191,6 @@ class AuthManager(component.Component):
os.rename(new_auth_file, old_auth_file)
self.__load_auth_file()
def __add_account(self, username, password, auth_level):
self.__auth[username] = Account(username, password, auth_level)
self.write_auth_file()
def __test_existing_account(self, username):
if username not in self.__auth:
# Let's try to re-load the file.. Maybe it's been updated
self.__load_auth_file()
if username not in self.__auth:
raise BadLoginError("Username does not exist")
return True
def __create_localclient_account(self):
"""
Returns the string.
@ -217,23 +239,27 @@ class AuthManager(component.Component):
log.warning("Your auth entry for %s contains no auth level, "
"using AUTH_LEVEL_DEFAULT(%s)..", username,
AUTH_LEVEL_DEFAULT)
auth_level = AUTH_LEVEL_DEFAULT
authlevel = AUTH_LEVEL_DEFAULT
elif len(lsplit) == 3:
username, password, auth_level = lsplit
username, password, authlevel = lsplit
else:
log.error("Your auth file is malformed: Incorrect number of fields!")
log.error("Your auth file is malformed: "
"Incorrect number of fields!")
continue
username = username.strip()
password = password.strip()
try:
auth_level = int(auth_level)
authlevel = int(authlevel)
except ValueError:
try:
authlevel = AUTH_LEVELS_MAPPING[authlevel]
except KeyError:
log.error("Your auth file is malformed: %r is not a valid auth "
"level" % auth_level)
"level" % authlevel)
continue
self.__auth[username] = Account(username, password, auth_level)
self.__auth[username] = Account(username, password, authlevel)
if "localclient" not in self.__auth:
self.__create_localclient_account()

View File

@ -2,6 +2,7 @@
# core.py
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
@ -43,8 +44,6 @@ import threading
import tempfile
from urlparse import urljoin
from twisted.internet import reactor, defer
from twisted.internet.task import LoopingCall
import twisted.web.client
import twisted.web.error
@ -55,7 +54,8 @@ import deluge.common
import deluge.component as component
from deluge.event import *
from deluge.error import *
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE
from deluge.core.authmanager import AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE
from deluge.core.torrentmanager import TorrentManager
from deluge.core.pluginmanager import PluginManager
from deluge.core.alertmanager import AlertManager
@ -77,7 +77,8 @@ class Core(component.Component):
log.info("Starting libtorrent %s session..", lt.version)
# Create the client fingerprint
version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")]
version = [int(value.split("-")[0]) for value in
deluge.common.get_version().split(".")]
while len(version) < 4:
version.append(0)
@ -147,16 +148,16 @@ class Core(component.Component):
def __save_session_state(self):
"""Saves the libtorrent session state"""
try:
open(deluge.configmanager.get_config_dir("session.state"), "wb").write(
lt.bencode(self.session.state()))
session_state = deluge.configmanager.get_config_dir("session.state")
open(session_state, "wb").write(lt.bencode(self.session.state()))
except Exception, e:
log.warning("Failed to save lt state: %s", e)
def __load_session_state(self):
"""Loads the libtorrent session state"""
try:
self.session.load_state(lt.bdecode(
open(deluge.configmanager.get_config_dir("session.state"), "rb").read()))
session_state = deluge.configmanager.get_config_dir("session.state")
self.session.load_state(lt.bdecode(open(session_state, "rb").read()))
except Exception, e:
log.warning("Failed to load lt state: %s", e)
@ -212,7 +213,9 @@ class Core(component.Component):
log.exception(e)
try:
torrent_id = self.torrentmanager.add(filedump=filedump, options=options, filename=filename)
torrent_id = self.torrentmanager.add(
filedump=filedump, options=options, filename=filename
)
except Exception, e:
log.error("There was an error adding the torrent file %s", filename)
log.exception(e)
@ -245,16 +248,23 @@ class Core(component.Component):
os.remove(filename)
except Exception, e:
log.warning("Couldn't remove temp file: %s", e)
return self.add_torrent_file(filename, base64.encodestring(data), options)
return self.add_torrent_file(
filename, base64.encodestring(data), options
)
def on_download_fail(failure):
if failure.check(twisted.web.error.PageRedirect):
new_url = urljoin(url, failure.getErrorMessage().split(" to ")[1])
result = download_file(new_url, tempfile.mkstemp()[1], headers=headers, force_filename=True)
result = download_file(
new_url, tempfile.mkstemp()[1], headers=headers,
force_filename=True
)
result.addCallbacks(on_download_success, on_download_fail)
elif failure.check(twisted.web.client.PartialDownloadError):
result = download_file(url, tempfile.mkstemp()[1], headers=headers, force_filename=True,
allow_compression=False)
result = download_file(
url, tempfile.mkstemp()[1], headers=headers,
force_filename=True, allow_compression=False
)
result.addCallbacks(on_download_success, on_download_fail)
else:
# Log the error and pass the failure onto the client
@ -263,7 +273,9 @@ class Core(component.Component):
result = failure
return result
d = download_file(url, tempfile.mkstemp()[1], headers=headers, force_filename=True)
d = download_file(
url, tempfile.mkstemp()[1], headers=headers, force_filename=True
)
d.addCallbacks(on_download_success, on_download_fail)
return d
@ -829,9 +841,22 @@ class Core(component.Component):
"""
return lt.version
@export(AUTH_LEVEL_DEFAULT)
@export(AUTH_LEVEL_ADMIN)
def get_known_accounts(self):
auth_level = component.get("RPCServer").get_session_auth_level()
return self.authmanager.get_known_accounts(
include_private_data=(auth_level==AUTH_LEVEL_ADMIN)
)
return self.authmanager.get_known_accounts()
@export(AUTH_LEVEL_NONE)
def get_auth_levels_mappings(self):
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
@export(AUTH_LEVEL_ADMIN)
def create_account(self, username, password, authlevel):
return self.authmanager.create_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN)
def update_account(self, username, password, authlevel):
return self.authmanager.update_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN)
def remove_account(self, username):
return self.authmanager.remove_account(username)

View File

@ -2,6 +2,7 @@
# error.py
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
@ -52,10 +53,9 @@ class InvalidPathError(DelugeError):
class NotAuthorizedError(DelugeError):
pass
class BadLoginError(DelugeError):
pass
class AuthenticationRequired(BadLoginError):
class _UsernameBasedException(DelugeError):
def _get_message(self):
return self._message
def _set_message(self, message):
@ -70,9 +70,17 @@ class AuthenticationRequired(BadLoginError):
username = property(_get_username, _set_username)
del _get_username, _set_username
def __init__(self, message, username):
super(AuthenticationRequired, self).__init__(message)
super(_UsernameBasedException, self).__init__(message)
self.message = message
self.username = username
class BadLoginError(_UsernameBasedException):
pass
class AuthenticationRequired(BadLoginError):
pass
class AuthManagerError(_UsernameBasedException):
pass

View File

@ -2,6 +2,7 @@
# client.py
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
@ -76,6 +77,20 @@ class DelugeRPCError(object):
self.exception_msg = exception_msg
self.traceback = traceback
def logable(self):
# Create a delugerpcrequest to print out a nice RPCRequest string
r = DelugeRPCRequest()
r.method = self.method
r.args = self.args
r.kwargs = self.kwargs
msg = "RPCError Message Received!"
msg += "\n" + "-" * 80
msg += "\n" + "RPCRequest: " + r.__repr__()
msg += "\n" + "-" * 80
msg += "\n" + self.traceback + "\n" + self.exception_type + ": " + self.exception_msg
msg += "\n" + "-" * 80
return msg
class DelugeRPCRequest(object):
"""
This object is created whenever there is a RPCRequest to be sent to the
@ -118,6 +133,7 @@ class DelugeRPCRequest(object):
return (self.request_id, self.method, self.args, self.kwargs)
class DelugeRPCProtocol(Protocol):
def connectionMade(self):
self.__rpc_requests = {}
self.__buffer = None
@ -273,6 +289,9 @@ class DaemonSSLProxy(DaemonProxy):
self.disconnect_deferred = None
self.disconnect_callback = None
self.auth_levels_mapping = None
self.auth_levels_mapping_reverse = None
def connect(self, host, port):
"""
Connects to a daemon at host:port
@ -402,21 +421,8 @@ class DaemonSSLProxy(DaemonProxy):
except:
pass
# Get the DelugeRPCError object from the error_data
error = error_data.value
# Create a delugerpcrequest to print out a nice RPCRequest string
r = DelugeRPCRequest()
r.method = error.method
r.args = error.args
r.kwargs = error.kwargs
msg = "RPCError Message Received!"
msg += "\n" + "-" * 80
msg += "\n" + "RPCRequest: " + r.__repr__()
msg += "\n" + "-" * 80
msg += "\n" + error.traceback + "\n" + error.exception_type + ": " + error.exception_msg
msg += "\n" + "-" * 80
log.error(msg)
if error_data.value.exception_type != 'AuthManagerError':
log.error(error_data.value.logable())
return error_data
def __on_connect(self, result):
@ -452,13 +458,24 @@ class DaemonSSLProxy(DaemonProxy):
self.authentication_level = result
# We need to tell the daemon what events we're interested in receiving
if self.__factory.event_handlers:
self.call("daemon.set_event_interest", self.__factory.event_handlers.keys())
self.call("daemon.set_event_interest",
self.__factory.event_handlers.keys())
self.call("core.get_auth_levels_mappings").addCallback(
self.__on_auth_levels_mappings
)
self.login_deferred.callback(result)
def __on_login_fail(self, result):
log.debug("_on_login_fail(): %s", result)
self.login_deferred.errback(result)
def __on_auth_levels_mappings(self, result):
auth_levels_mapping, auth_levels_mapping_reverse = result
self.auth_levels_mapping = auth_levels_mapping
self.auth_levels_mapping_reverse = auth_levels_mapping_reverse
def set_disconnect_callback(self, cb):
"""
Set a function to be called when the connection to the daemon is lost
@ -481,9 +498,13 @@ class DaemonClassicProxy(DaemonProxy):
self.host = "localhost"
self.port = 58846
# Running in classic mode, it's safe to import auth level
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
from deluge.core.authmanager import (AUTH_LEVEL_ADMIN,
AUTH_LEVELS_MAPPING,
AUTH_LEVELS_MAPPING_REVERSE)
self.username = "localclient"
self.authentication_level = AUTH_LEVEL_ADMIN
self.auth_levels_mapping = AUTH_LEVELS_MAPPING
self.auth_levels_mapping_reverse = AUTH_LEVELS_MAPPING_REVERSE
# Register the event handlers
for event in event_handlers:
for handler in event_handlers[event]:
@ -776,5 +797,13 @@ class Client(object):
"""
return self._daemon_proxy.authentication_level
@property
def auth_levels_mapping(self):
return self._daemon_proxy.auth_levels_mapping
@property
def auth_levels_mapping_reverse(self):
return self._daemon_proxy.auth_levels_mapping_reverse
# This is the object clients will use
client = Client()

View File

@ -176,7 +176,7 @@ class ErrorDialog(BaseDialog):
details = tb
if details:
self.set_default_size(500, 400)
self.set_default_size(600, 400)
textview = gtk.TextView()
textview.set_editable(False)
textview.get_buffer().set_text(details)
@ -245,3 +245,87 @@ class AuthenticationDialog(BaseDialog):
def on_password_activate(self, widget):
self.response(gtk.RESPONSE_OK)
class AccountDialog(BaseDialog):
def __init__(self, username=None, password=None, authlevel=None,
levels_mapping=None, parent=None):
if username:
super(AccountDialog, self).__init__(
_("Edit Account"),
_("Edit existing account"),
gtk.STOCK_DIALOG_INFO,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_APPLY, gtk.RESPONSE_OK),
parent)
else:
super(AccountDialog, self).__init__(
_("New Account"),
_("Create a new account"),
gtk.STOCK_DIALOG_INFO,
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
gtk.STOCK_ADD, gtk.RESPONSE_OK),
parent)
self.levels_mapping = levels_mapping
table = gtk.Table(2, 3, False)
self.username_label = gtk.Label()
self.username_label.set_markup(_("<b>Username:</b>"))
self.username_label.set_alignment(1.0, 0.5)
self.username_label.set_padding(5, 5)
self.username_entry = gtk.Entry()
table.attach(self.username_label, 0, 1, 0, 1)
table.attach(self.username_entry, 1, 2, 0, 1)
self.authlevel_label = gtk.Label()
self.authlevel_label.set_markup(_("<b>Authentication Level:</b>"))
self.authlevel_label.set_alignment(1.0, 0.5)
self.authlevel_label.set_padding(5, 5)
self.authlevel_combo = gtk.combo_box_new_text()
active_idx = None
for idx, level in enumerate(levels_mapping.keys()):
self.authlevel_combo.append_text(level)
if authlevel and authlevel==level:
active_idx = idx
elif not authlevel and level == 'DEFAULT':
active_idx = idx
print 'aidx', active_idx
if active_idx is not None:
self.authlevel_combo.set_active(active_idx)
table.attach(self.authlevel_label, 0, 1, 1, 2)
table.attach(self.authlevel_combo, 1, 2, 1, 2)
self.password_label = gtk.Label()
self.password_label.set_markup(_("<b>Password:</b>"))
self.password_label.set_alignment(1.0, 0.5)
self.password_label.set_padding(5, 5)
self.password_entry = gtk.Entry()
self.password_entry.set_visibility(False)
table.attach(self.password_label, 0, 1, 2, 3)
table.attach(self.password_entry, 1, 2, 2, 3)
self.vbox.pack_start(table, False, False, padding=5)
if username:
self.username_entry.set_text(username)
self.username_entry.set_editable(False)
else:
self.set_focus(self.username_entry)
if password:
self.password_entry.set_text(username)
self.show_all()
def get_username(self):
return self.username_entry.get_text()
def get_password(self):
return self.password_entry.get_text()
def get_authlevel(self):
combobox = self.authlevel_combo
level = combobox.get_model()[combobox.get_active()][0]
return level

View File

@ -1,4 +1,4 @@
<?xml version="1.0"?>
<?xml version="1.0" encoding="UTF-8"?>
<glade-interface>
<!-- interface-requires gtk+ 2.12 -->
<!-- interface-naming-policy toplevel-contextual -->
@ -11,13 +11,11 @@
<property name="default_height">530</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="has_separator">False</property>
<signal name="delete_event" handler="on_pref_dialog_delete_event"/>
<child internal-child="vbox">
<widget class="GtkVBox" id="dialog-vbox1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<widget class="GtkHPaned" id="hpaned1">
@ -50,7 +48,7 @@
<property name="show_tabs">False</property>
<property name="scrollable">True</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow1">
<widget class="GtkScrolledWindow" id="DownloadsScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -66,7 +64,6 @@
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label21">
<property name="visible">True</property>
@ -294,7 +291,7 @@
<widget class="GtkEntry" id="entry_torrents_path">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="invisible_char">&#x25CF;</property>
<property name="invisible_char"></property>
</widget>
<packing>
<property name="position">1</property>
@ -501,7 +498,7 @@
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow2">
<widget class="GtkScrolledWindow" id="NetworkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -517,7 +514,6 @@
<widget class="GtkVBox" id="vbox2">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label22">
<property name="visible">True</property>
@ -557,7 +553,6 @@
<child>
<widget class="GtkVBox" id="vbox3">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkHBox" id="hbox2">
<property name="visible">True</property>
@ -755,7 +750,6 @@
<child>
<widget class="GtkVBox" id="vbox25">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<widget class="GtkCheckButton" id="chk_random_outgoing_ports">
@ -880,7 +874,7 @@
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">Enter the IP address of the interface to listen for incoming bittorrent connections on. Leave this empty if you want to use the default.</property>
<property name="max_length">60</property>
<property name="invisible_char">&#x25CF;</property>
<property name="invisible_char"></property>
<property name="width_chars">30</property>
</widget>
<packing>
@ -927,7 +921,6 @@
<child>
<widget class="GtkVBox" id="vbox4">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkHBox" id="hbox4">
<property name="visible">True</property>
@ -1127,7 +1120,6 @@
<widget class="GtkVBox" id="vbox10">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label7">
<property name="visible">True</property>
@ -1159,7 +1151,6 @@
<widget class="GtkVBox" id="vbox12">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkComboBox" id="combo_encin">
<property name="visible">True</property>
@ -1193,7 +1184,6 @@ Either</property>
<widget class="GtkVBox" id="vbox15">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkHBox" id="hbox15">
<property name="visible">True</property>
@ -1291,7 +1281,7 @@ Disabled</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow3">
<widget class="GtkScrolledWindow" id="BandwidthScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -1307,7 +1297,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox7">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label23">
<property name="visible">True</property>
@ -1347,7 +1336,6 @@ Disabled</property>
<child>
<widget class="GtkVBox" id="vbox21">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<widget class="GtkTable" id="table1">
@ -1361,7 +1349,7 @@ Disabled</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9999 1 10 0</property>
<property name="adjustment">0 -1 9999 1 10 0</property>
<property name="numeric">True</property>
</widget>
<packing>
@ -1377,7 +1365,7 @@ Disabled</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9999 1 10 0</property>
<property name="adjustment">0 -1 9999 1 10 0</property>
<property name="numeric">True</property>
</widget>
<packing>
@ -1456,7 +1444,7 @@ Disabled</property>
<property name="tooltip" translatable="yes">The maximum number of connections allowed. Set -1 for unlimited.</property>
<property name="max_length">4</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9000 1 10 0</property>
<property name="adjustment">0 -1 9000 1 10 0</property>
<property name="climb_rate">1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
@ -1488,7 +1476,7 @@ Disabled</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">The maximum download speed for all torrents. Set -1 for unlimited.</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 60000 1 10 0</property>
<property name="adjustment">0 -1 60000 1 10 0</property>
<property name="climb_rate">1</property>
<property name="digits">1</property>
<property name="numeric">True</property>
@ -1507,7 +1495,7 @@ Disabled</property>
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">The maximum upload speed for all torrents. Set -1 for unlimited.</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 60000 1 10 0</property>
<property name="adjustment">0 -1 60000 1 10 0</property>
<property name="climb_rate">1</property>
<property name="digits">1</property>
<property name="numeric">True</property>
@ -1526,7 +1514,7 @@ Disabled</property>
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">The maximum upload slots for all torrents. Set -1 for unlimited.</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9000 1 10 0</property>
<property name="adjustment">0 -1 9000 1 10 0</property>
<property name="climb_rate">1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
@ -1631,7 +1619,7 @@ Disabled</property>
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">The maximum upload slots per torrent. Set -1 for unlimited.</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9000 1 10 0</property>
<property name="adjustment">0 -1 9000 1 10 0</property>
<property name="climb_rate">1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
@ -1650,7 +1638,7 @@ Disabled</property>
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">The maximum number of connections per torrent. Set -1 for unlimited.</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9000 1 10 0</property>
<property name="adjustment">0 -1 9000 1 10 0</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</widget>
@ -1716,7 +1704,7 @@ Disabled</property>
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">The maximum number of connections per torrent. Set -1 for unlimited.</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9000 1 10 0</property>
<property name="adjustment">0 -1 9000 1 10 0</property>
<property name="digits">1</property>
<property name="numeric">True</property>
</widget>
@ -1734,7 +1722,7 @@ Disabled</property>
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">The maximum number of connections per torrent. Set -1 for unlimited.</property>
<property name="xalign">1</property>
<property name="adjustment">-1 -1 9000 1 10 0</property>
<property name="adjustment">0 -1 9000 1 10 0</property>
<property name="digits">1</property>
<property name="numeric">True</property>
</widget>
@ -1790,7 +1778,7 @@ Disabled</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow4">
<widget class="GtkScrolledWindow" id="InterfaceScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -1806,7 +1794,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox8">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label25">
<property name="visible">True</property>
@ -1892,7 +1879,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox27">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkCheckButton" id="chk_show_rate_in_title">
<property name="label" translatable="yes">Show session speed in titlebar</property>
@ -1943,7 +1929,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox5">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkCheckButton" id="chk_show_dialog">
<property name="label" translatable="yes">Always show</property>
@ -2013,7 +1998,6 @@ Disabled</property>
<child>
<widget class="GtkVBox" id="vbox17">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkCheckButton" id="chk_use_tray">
<property name="label" translatable="yes">Enable system tray icon</property>
@ -2196,7 +2180,7 @@ Disabled</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow7">
<widget class="GtkScrolledWindow" id="OtherScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -2212,7 +2196,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox16">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label40">
<property name="visible">True</property>
@ -2254,7 +2237,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox18">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkAlignment" id="alignment36">
<property name="visible">True</property>
@ -2316,7 +2298,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox19">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label44">
<property name="visible">True</property>
@ -2392,7 +2373,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox29">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkAlignment" id="alignment50">
<property name="visible">True</property>
@ -2417,7 +2397,7 @@ Disabled</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip" translatable="yes">If Deluge cannot find the database file at this location it will fallback to using DNS to resolve the peer's country.</property>
<property name="invisible_char">&#x25CF;</property>
<property name="invisible_char"></property>
</widget>
<packing>
<property name="position">1</property>
@ -2530,7 +2510,7 @@ Disabled</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow6">
<widget class="GtkScrolledWindow" id="DaemonScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -2546,7 +2526,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox13">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label34">
<property name="visible">True</property>
@ -2588,7 +2567,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox14">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkHBox" id="hbox12">
<property name="visible">True</property>
@ -2736,6 +2714,111 @@ Disabled</property>
<property name="position">4</property>
</packing>
</child>
<child>
<widget class="GtkFrame" id="AccountsFrame">
<property name="visible">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment33">
<property name="visible">True</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkHBox" id="hbox22">
<property name="visible">True</property>
<child>
<widget class="GtkTreeView" id="accounts_listview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkVButtonBox" id="vbuttonbox1">
<property name="visible">True</property>
<property name="spacing">5</property>
<property name="homogeneous">True</property>
<property name="layout_style">start</property>
<child>
<widget class="GtkButton" id="accounts_add">
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_accounts_add_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="accounts_edit">
<property name="label">gtk-edit</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_accounts_edit_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="accounts_delete">
<property name="label">gtk-delete</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_accounts_delete_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="padding">4</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="label61">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="xalign">0</property>
<property name="xpad">10</property>
<property name="ypad">10</property>
<property name="label" translatable="yes">&lt;b&gt;Accounts&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</child>
</widget>
<packing>
<property name="position">5</property>
</packing>
</child>
</widget>
</child>
</widget>
@ -2757,7 +2840,7 @@ Disabled</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow8">
<widget class="GtkScrolledWindow" id="QueueScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -2773,7 +2856,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox9">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label46">
<property name="visible">True</property>
@ -2803,7 +2885,6 @@ Disabled</property>
<widget class="GtkVBox" id="queue_prefs_box2">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<widget class="GtkFrame" id="frame10">
@ -2821,7 +2902,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox6">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkCheckButton" id="chk_queue_new_top">
<property name="label" translatable="yes">Queue new torrents to top</property>
@ -2872,7 +2952,6 @@ Disabled</property>
<child>
<widget class="GtkVBox" id="vbox20">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<widget class="GtkTable" id="table3">
@ -3024,7 +3103,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox11">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<widget class="GtkTable" id="table2">
@ -3234,7 +3312,7 @@ Disabled</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow9">
<widget class="GtkScrolledWindow" id="ProxyScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -3250,7 +3328,6 @@ Disabled</property>
<widget class="GtkVBox" id="vbox22">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label83">
<property name="visible">True</property>
@ -3278,7 +3355,6 @@ Disabled</property>
<child>
<widget class="GtkVBox" id="vbox26">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<widget class="GtkFrame" id="frame23">
@ -3372,7 +3448,7 @@ Disabled</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="adjustment">8080 0 65535 1 10 0</property>
<property name="adjustment">100 0 65535 1 10 0</property>
<property name="numeric">True</property>
</widget>
</child>
@ -3556,7 +3632,7 @@ HTTP W/ Auth</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="adjustment">8080 0 65535 1 10 0</property>
<property name="adjustment">100 0 65535 1 10 0</property>
<property name="numeric">True</property>
</widget>
</child>
@ -3740,7 +3816,7 @@ HTTP W/ Auth</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="adjustment">8080 0 65535 1 10 0</property>
<property name="adjustment">100 0 65535 1 10 0</property>
<property name="numeric">True</property>
</widget>
</child>
@ -3927,7 +4003,7 @@ HTTP W/ Auth</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="adjustment">8080 0 65535 1 10 0</property>
<property name="adjustment">100 0 65535 1 10 0</property>
<property name="numeric">True</property>
</widget>
</child>
@ -4042,7 +4118,7 @@ HTTP W/ Auth</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow13">
<widget class="GtkScrolledWindow" id="CacheScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property>
@ -4055,7 +4131,6 @@ HTTP W/ Auth</property>
<child>
<widget class="GtkVBox" id="vbox30">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label111">
<property name="visible">True</property>
@ -4087,7 +4162,6 @@ HTTP W/ Auth</property>
<child>
<widget class="GtkVBox" id="vbox31">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkFrame" id="frame32">
<property name="visible">True</property>
@ -4132,9 +4206,9 @@ HTTP W/ Auth</property>
<widget class="GtkSpinButton" id="spin_cache_size">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
<property name="invisible_char"></property>
<property name="xalign">1</property>
<property name="adjustment">512 0 99999 1 10 0</property>
<property name="adjustment">100 0 99999 1 10 0</property>
<property name="numeric">True</property>
<property name="update_policy">if-valid</property>
</widget>
@ -4149,7 +4223,7 @@ HTTP W/ Auth</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="max_length">5</property>
<property name="invisible_char">&#x25CF;</property>
<property name="invisible_char"></property>
<property name="width_chars">5</property>
<property name="xalign">1</property>
<property name="adjustment">60 1 32000 1 10 0</property>
@ -4197,7 +4271,6 @@ HTTP W/ Auth</property>
<child>
<widget class="GtkVBox" id="vbox32">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkFrame" id="frame34">
<property name="visible">True</property>
@ -4590,7 +4663,7 @@ HTTP W/ Auth</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow5">
<widget class="GtkScrolledWindow" id="PluginsScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@ -4606,7 +4679,6 @@ HTTP W/ Auth</property>
<widget class="GtkVBox" id="vbox28">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkLabel" id="label51">
<property name="visible">True</property>
@ -4637,7 +4709,6 @@ HTTP W/ Auth</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow12">
<property name="visible">True</property>
@ -4996,7 +5067,7 @@ HTTP W/ Auth</property>
</child>
</widget>
<packing>
<property name="position">9</property>
<property name="position">2</property>
</packing>
</child>
<child>

View File

@ -2,6 +2,7 @@
# menubar.py
#
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
@ -212,7 +213,9 @@ class MenuBar(component.Component):
self.menuitem_change_owner.set_visible(False)
# Get Known accounts to allow chaning ownership
client.core.get_known_accounts().addCallback(self._on_known_accounts)
client.core.get_known_accounts().addCallback(
self._on_known_accounts).addErrback(self._on_known_accounts_fail
)
def stop(self):
log.debug("MenuBar stopping")
@ -354,9 +357,9 @@ class MenuBar(component.Component):
def show_move_storage_dialog(self, status):
log.debug("show_move_storage_dialog")
glade = gtk.glade.XML(
pkg_resources.resource_filename("deluge.ui.gtkui",
"glade/move_storage_dialog.glade"))
glade = gtk.glade.XML(pkg_resources.resource_filename(
"deluge.ui.gtkui", "glade/move_storage_dialog.glade"
))
dialog = glade.get_widget("move_storage_dialog")
dialog.set_transient_for(self.window.window)
entry = glade.get_widget("entry_destination")
@ -439,10 +442,21 @@ class MenuBar(component.Component):
}
# widget: (header, type_str, image_stockid, image_filename, default)
other_dialog_info = {
"menuitem_down_speed": (_("Set Maximum Download Speed"), "KiB/s", None, "downloading.svg", -1.0),
"menuitem_up_speed": (_("Set Maximum Upload Speed"), "KiB/s", None, "seeding.svg", -1.0),
"menuitem_max_connections": (_("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, -1),
"menuitem_upload_slots": (_("Set Maximum Upload Slots"), "", gtk.STOCK_SORT_ASCENDING, None, -1)
"menuitem_down_speed": (
_("Set Maximum Download Speed"),
"KiB/s", None, "downloading.svg", -1.0
),
"menuitem_up_speed": (
_("Set Maximum Upload Speed"),
"KiB/s", None, "seeding.svg", -1.0
),
"menuitem_max_connections": (
_("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, -1
),
"menuitem_upload_slots": (
_("Set Maximum Upload Slots"),
"", gtk.STOCK_SORT_ASCENDING, None, -1
)
}
# Show the other dialog
@ -483,7 +497,15 @@ class MenuBar(component.Component):
getattr(self.window.main_glade.get_widget(item), attr)()
def _on_known_accounts(self, known_accounts):
log.debug("_on_known_accounts: %s", known_accounts)
known_accounts_to_log = []
for account in known_accounts:
account_to_log = {}
for key, value in account.copy().iteritems():
if key == 'password':
value = '*' * len(value)
account_to_log[key] = value
known_accounts_to_log.append(account_to_log)
log.debug("_on_known_accounts: %s", known_accounts_to_log)
if len(known_accounts) <= 1:
return
@ -496,14 +518,18 @@ class MenuBar(component.Component):
self.change_owner_submenu_items[None] = gtk.RadioMenuItem(maingroup)
for account in known_accounts:
self.change_owner_submenu_items[account] = item = gtk.RadioMenuItem(maingroup, account)
username = account["username"]
item = gtk.RadioMenuItem(maingroup, username)
self.change_owner_submenu_items[username] = item
self.change_owner_submenu.append(item)
item.connect("toggled", self._on_change_owner_toggled, account)
item.connect("toggled", self._on_change_owner_toggled, username)
self.change_owner_submenu.show_all()
self.change_owner_submenu_items[None].set_active(True)
self.change_owner_submenu_items[None].hide()
self.menuitem_change_owner.connect("activate", self._on_change_owner_submenu_active)
self.menuitem_change_owner.connect(
"activate", self._on_change_owner_submenu_active
)
self.menuitem_change_owner.set_submenu(self.change_owner_submenu)
def _on_known_accounts_fail(self, reason):
@ -517,18 +543,18 @@ class MenuBar(component.Component):
return
torrent_owner = component.get("TorrentView").get_torrent_status(selected[0])["owner"]
for account, item in self.change_owner_submenu_items.iteritems():
item.set_active(account == torrent_owner)
for username, item in self.change_owner_submenu_items.iteritems():
item.set_active(username == torrent_owner)
def _on_change_owner_toggled(self, widget, account):
def _on_change_owner_toggled(self, widget, username):
log.debug("_on_change_owner_toggled")
update_torrents = []
selected = component.get("TorrentView").get_selected_torrents()
for torrent_id in selected:
torrent_status = component.get("TorrentView").get_torrent_status(torrent_id)
if torrent_status["owner"] != account:
if torrent_status["owner"] != username:
update_torrents.append(torrent_id)
if update_torrents:
log.debug("Setting torrent owner \"%s\" on %s", account, update_torrents)
client.core.set_torrents_owner(update_torrents, account)
log.debug("Setting torrent owner \"%s\" on %s", username, update_torrents)
client.core.set_torrents_owner(update_torrents, username)

View File

@ -2,6 +2,7 @@
# preferences.py
#
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <ufs@ufsoft.org>
#
# Deluge is free software.
#
@ -46,11 +47,14 @@ from deluge.ui.client import client
import deluge.common
import deluge.error
import common
import dialogs
from deluge.configmanager import ConfigManager
import deluge.configmanager
log = logging.getLogger(__name__)
ACCOUNTS_USERNAME, ACCOUNTS_LEVEL, ACCOUNTS_PASSWORD = range(3)
class Preferences(component.Component):
def __init__(self):
component.Component.__init__(self, "Preferences")
@ -81,6 +85,35 @@ class Preferences(component.Component):
self.liststore.append([i, category])
i += 1
# Setup accounts tab lisview
self.accounts_levels_mapping = None
self.accounts_authlevel = self.glade.get_widget("accounts_authlevel")
self.accounts_liststore = gtk.ListStore(str, str, str, int)
self.accounts_liststore.set_sort_column_id(ACCOUNTS_USERNAME,
gtk.SORT_ASCENDING)
self.accounts_listview = self.glade.get_widget("accounts_listview")
self.accounts_listview.append_column(
gtk.TreeViewColumn(
_("Username"), gtk.CellRendererText(), text=ACCOUNTS_USERNAME
)
)
self.accounts_listview.append_column(
gtk.TreeViewColumn(
_("Level"), gtk.CellRendererText(), text=ACCOUNTS_LEVEL
)
)
password_column = gtk.TreeViewColumn(
'password', gtk.CellRendererText(), text=ACCOUNTS_PASSWORD
)
self.accounts_listview.append_column(password_column)
password_column.set_visible(False)
self.accounts_listview.set_model(self.accounts_liststore)
self.accounts_listview.get_selection().connect(
"changed", self._on_accounts_selection_changed
)
self.accounts_frame = self.glade.get_widget("AccountsFrame")
# Setup plugin tab listview
self.plugin_liststore = gtk.ListStore(str, bool)
self.plugin_liststore.set_sort_column_id(0, gtk.SORT_ASCENDING)
@ -96,11 +129,13 @@ class Preferences(component.Component):
# Connect to the 'changed' event of TreeViewSelection to get selection
# changes.
self.treeview.get_selection().connect("changed",
self.on_selection_changed)
self.treeview.get_selection().connect(
"changed", self.on_selection_changed
)
self.plugin_listview.get_selection().connect("changed",
self.on_plugin_selection_changed)
self.plugin_listview.get_selection().connect(
"changed", self.on_plugin_selection_changed
)
self.glade.signal_autoconnect({
"on_pref_dialog_delete_event": self.on_pref_dialog_delete_event,
@ -114,7 +149,10 @@ class Preferences(component.Component):
"on_button_find_plugins_clicked": self._on_button_find_plugins_clicked,
"on_button_cache_refresh_clicked": self._on_button_cache_refresh_clicked,
"on_combo_proxy_type_changed": self._on_combo_proxy_type_changed,
"on_button_associate_magnet_clicked": self._on_button_associate_magnet_clicked
"on_button_associate_magnet_clicked": self._on_button_associate_magnet_clicked,
"on_accounts_add_clicked": self._on_accounts_add_clicked,
"on_accounts_delete_clicked": self._on_accounts_delete_clicked,
"on_accounts_edit_clicked": self._on_accounts_edit_clicked
})
# These get updated by requests done to the core
@ -191,9 +229,12 @@ class Preferences(component.Component):
component.get("PluginManager").run_on_show_prefs()
# Update the preferences dialog to reflect current config settings
self.core_config = {}
if client.connected():
self._get_accounts_tab_data()
def _on_get_config(config):
self.core_config = config
client.core.get_available_plugins().addCallback(_on_get_available_plugins)
@ -832,6 +873,9 @@ class Preferences(component.Component):
# Show the correct notebook page based on what row is selected.
(model, row) = treeselection.get_selected()
try:
if model.get_value(row, 1) == _("Daemon"):
# Let's see update the accounts related stuff
self._get_accounts_tab_data()
self.notebook.set_current_page(model.get_value(row, 0))
except TypeError:
pass
@ -961,3 +1005,169 @@ class Preferences(component.Component):
def _on_button_associate_magnet_clicked(self, widget):
common.associate_magnet_links(True)
def _get_accounts_tab_data(self):
def on_ok(accounts):
self.accounts_frame.show()
self._on_get_known_accounts(accounts)
def on_fail(failure):
if failure.value.exception_type == 'NotAuthorizedError':
self.accounts_frame.hide()
else:
dialogs.ErrorDialog(
_("Server Side Error"),
_("An error ocurred on the server"),
self.pref_dialog, details=failure.value.logable()
).run()
client.core.get_known_accounts().addCallback(on_ok).addErrback(on_fail)
def _on_get_known_accounts(self, known_accounts):
known_accounts_to_log = []
for account in known_accounts:
account_to_log = {}
for key, value in account.copy().iteritems():
if key == 'password':
value = '*' * len(value)
account_to_log[key] = value
known_accounts_to_log.append(account_to_log)
log.debug("_on_known_accounts: %s", known_accounts_to_log)
self.accounts_liststore.clear()
for account in known_accounts:
iter = self.accounts_liststore.append()
self.accounts_liststore.set_value(
iter, ACCOUNTS_USERNAME, account['username']
)
self.accounts_liststore.set_value(
iter, ACCOUNTS_LEVEL, account['authlevel']
)
self.accounts_liststore.set_value(
iter, ACCOUNTS_PASSWORD, account['password']
)
def _on_accounts_selection_changed(self, treeselection):
log.debug("_on_accounts_selection_changed")
(model, itr) = treeselection.get_selected()
if not itr:
return
username = model[itr][0]
if username:
self.glade.get_widget("accounts_edit").set_sensitive(True)
self.glade.get_widget("accounts_delete").set_sensitive(True)
else:
self.glade.get_widget("accounts_edit").set_sensitive(False)
self.glade.get_widget("accounts_delete").set_sensitive(False)
def _on_accounts_add_clicked(self, widget):
dialog = dialogs.AccountDialog(
levels_mapping=client.auth_levels_mapping,
parent=self.pref_dialog
)
def dialog_finished(response_id):
username = dialog.get_username()
password = dialog.get_password()
authlevel = dialog.get_authlevel()
def add_ok(rv):
iter = self.accounts_liststore.append()
self.accounts_liststore.set_value(
iter, ACCOUNTS_USERNAME, username
)
self.accounts_liststore.set_value(
iter, ACCOUNTS_LEVEL, authlevel
)
self.accounts_liststore.set_value(
iter, ACCOUNTS_PASSWORD, password
)
def add_fail(failure):
if failure.value.exception_type == 'AuthManagerError':
dialogs.ErrorDialog(
_("Error Adding Account"),
failure.value.exception_msg
).run()
else:
dialogs.ErrorDialog(
_("Error Adding Account"),
_("An error ocurred while adding account"),
self.pref_dialog, details=failure.value.logable()
).run()
if response_id == gtk.RESPONSE_OK:
client.core.create_account(
username, password, authlevel
).addCallback(add_ok).addErrback(add_fail)
dialog.run().addCallback(dialog_finished)
def _on_accounts_edit_clicked(self, widget):
(model, itr) = self.accounts_listview.get_selection().get_selected()
if not itr:
return
dialog = dialogs.AccountDialog(
model[itr][ACCOUNTS_USERNAME],
model[itr][ACCOUNTS_PASSWORD],
model[itr][ACCOUNTS_LEVEL],
levels_mapping=client.auth_levels_mapping,
parent=self.pref_dialog
)
def dialog_finished(response_id):
def update_ok(rc):
model.set_value(itr, ACCOUNTS_PASSWORD, dialog.get_username())
model.set_value(itr, ACCOUNTS_LEVEL, dialog.get_authlevel())
def update_fail(failure):
dialogs.ErrorDialog(
_("Error Updating Account"),
_("An error ocurred while updating account"),
self.pref_dialog, details=failure.value.logable()
).run()
if response_id == gtk.RESPONSE_OK:
client.core.update_account(
dialog.get_username(),
dialog.get_password(),
dialog.get_authlevel()
).addCallback(update_ok).addErrback(update_fail)
dialog.run().addCallback(dialog_finished)
def _on_accounts_delete_clicked(self, widget):
(model, itr) = self.accounts_listview.get_selection().get_selected()
if not itr:
return
username = model[itr][0]
header = _("Remove Account")
text = _("Are you sure you wan't do remove the account with the "
"username \"%(username)s\"?" % dict(username=username))
dialog = dialogs.YesNoDialog(header, text, parent=self.pref_dialog)
def dialog_finished(response_id):
def remove_ok(rc):
model.remove(itr)
def remove_fail(failure):
if failure.value.exception_type == 'AuthManagerError':
dialogs.ErrorDialog(
_("Error Removing Account"),
failure.value.exception_msg
).run()
else:
dialogs.ErrorDialog(
_("Error Removing Account"),
_("An error ocurred while removing account"),
self.pref_dialog, details=failure.value.logable()
).run()
if response_id == gtk.RESPONSE_YES:
client.core.remove_account(
username
).addCallback(remove_ok).addErrback(remove_fail)
dialog.run().addCallback(dialog_finished)