From ea7ef950a39560ad9720082a9855e446260f52df Mon Sep 17 00:00:00 2001 From: Calum Lind Date: Sat, 22 Feb 2014 13:07:33 +0000 Subject: [PATCH] [GTKUI] Add new OtherDialog to dialogs This adds a new OtherDialog to dialogs so that will use Deferred to prevent the dialog loop locking up the mainwindow. Remove old `Other` dialog from common and cleanup up the file. Fixes #2401, context menus for torrents not showing current value. Fixes #2400, add a stop_at_ratio context menu. Change the protocol rate to display as int. --- deluge/ui/gtkui/common.py | 263 ++++++------------ deluge/ui/gtkui/dialogs.py | 90 +++++- deluge/ui/gtkui/glade/torrent_menu.options.ui | 9 + deluge/ui/gtkui/menubar.py | 102 ++++--- deluge/ui/gtkui/statusbar.py | 101 +++---- deluge/ui/gtkui/systemtray.py | 77 ++--- 6 files changed, 336 insertions(+), 306 deletions(-) diff --git a/deluge/ui/gtkui/common.py b/deluge/ui/gtkui/common.py index edab86683..b36cce737 100644 --- a/deluge/ui/gtkui/common.py +++ b/deluge/ui/gtkui/common.py @@ -1,128 +1,93 @@ -# -# common.py +# -*- coding: utf-8 -*- # # Copyright (C) 2008 Marcos Pinto ('markybob') # -# Deluge is free software. +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. # -# You may redistribute it and/or modify it under the terms of the -# GNU General Public License, as published by the Free Software -# Foundation; either version 3 of the License, or (at your option) -# any later version. -# -# deluge is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with deluge. If not, write to: -# The Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor -# Boston, MA 02110-1301, USA. -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. -# -# - """Common functions for various parts of gtkui to use.""" import os - -import pygtk -pygtk.require('2.0') -import gtk import logging import cPickle import shutil -import deluge.component as component +import pygtk +pygtk.require('2.0') +import gtk +from gobject import GError + import deluge.common log = logging.getLogger(__name__) + def get_logo(size): - """Returns a deluge logo pixbuf based on the size parameter.""" + """A Deluge logo. + + Params: + size (int): Size of logo in pixels + + Returns: + gtk.gdk.Pixbuf: deluge logo + """ + filename = "deluge.svg" if deluge.common.windows_check() or deluge.common.osx_check(): - return gtk.gdk.pixbuf_new_from_file_at_size(deluge.common.get_pixmap("deluge.png"), \ - size, size) - else: - try: - return gtk.gdk.pixbuf_new_from_file_at_size(deluge.common.get_pixmap("deluge.svg"), \ - size, size) - except Exception, e: - log.warning(e) + filename = "deluge.png" + try: + return gtk.gdk.pixbuf_new_from_file_at_size(deluge.common.get_pixmap(filename), size, size) + except GError as ex: + log.warning(ex) -def build_menu_radio_list(value_list, callback, pref_value=None, - suffix=None, show_notset=False, notset_label=None, notset_lessthan=0, - show_other=False, show_activated=False, activated_label=None): - # Build a menu with radio menu items from a list and connect them to - # the callback. The pref_value is what you would like to test for the - # default active radio item. - if notset_label is None: - notset_label = _("Unlimited") - if activated_label is None: - activated_label = _("Activated") +def build_menu_radio_list(value_list, callback, pref_value=None, suffix=None, show_notset=False, + notset_label="∞", notset_lessthan=0, show_other=False): + """Build a menu with radio menu items from a list and connect them to the callback. + Params: + value_list [list]: List of values to build into a menu. + callback (function): The function to call when menu item is clicked. + pref_value (int): A preferred value to insert into value_list + suffix (str): Append a suffix the the menu items in value_list. + show_notset (bool): Show the unlimited menu item. + notset_label (str): The text for the unlimited menu item. + notset_lessthan (int): Activates the unlimited menu item if pref_value is less than this. + show_other (bool): Show the `Other` menu item. + + The pref_value is what you would like to test for the default active radio item. + + Returns: + gtk.Menu: The menu radio + """ menu = gtk.Menu() group = None - if show_activated is False: - if pref_value > -1 and pref_value not in value_list: - value_list.pop() - value_list.append(pref_value) - for value in sorted(value_list): - if suffix != None: - menuitem = gtk.RadioMenuItem(group, str(value) + " " + \ - suffix) - else: - menuitem = gtk.RadioMenuItem(group, str(value)) + if pref_value > -1 and pref_value not in value_list: + value_list.pop() + value_list.append(pref_value) - group = menuitem - - if value == pref_value and pref_value != None: - menuitem.set_active(True) - - if callback != None: - menuitem.connect("toggled", callback) - - menu.append(menuitem) - - if show_activated is True: - for value in sorted(value_list): - menuitem = gtk.RadioMenuItem(group, str(activated_label)) - - group = menuitem - - if value == pref_value and pref_value != None: - menuitem.set_active(True) - - if callback != None: - menuitem.connect("toggled", callback) - - menu.append(menuitem) + for value in sorted(value_list): + item_text = str(value) + if suffix: + item_text += " " + suffix + menuitem = gtk.RadioMenuItem(group, item_text) + group = menuitem + if pref_value and value == pref_value: + menuitem.set_active(True) + if callback: + menuitem.connect("toggled", callback) + menu.append(menuitem) if show_notset: menuitem = gtk.RadioMenuItem(group, notset_label) menuitem.set_name("unlimited") - if pref_value < notset_lessthan and pref_value != None: - menuitem.set_active(True) - if show_activated and pref_value == 1: + if pref_value and pref_value < notset_lessthan: menuitem.set_active(True) menuitem.connect("toggled", callback) menu.append(menuitem) - # Add the Other... menuitem - if show_other is True: + if show_other: menuitem = gtk.SeparatorMenuItem() menu.append(menuitem) menuitem = gtk.MenuItem(_("Other...")) @@ -132,83 +97,25 @@ def build_menu_radio_list(value_list, callback, pref_value=None, return menu -def show_other_dialog(header, type_str, image_stockid=None, image_filename=None, default=0): - """ - Shows a dialog with `header` as the header text and `type_str` - as the type text. The type of spinbutton (int or float) is determined by - `default` type. - - :param header: str, the header label text - :param type_str: str, the type label text, what comes after the spinbutton - :param image_stockid: gtkStockId, the stock id of the image in the header - :param image_filename: str, filename of icon in pixmaps folder - :param default: the default value in the spinbutton - - :returns: None, int or float from spinbutton depending on `default`. - None is returned if the user clicks on Cancel. - :rtype: None, int or float - - :raises TypeError: if `default` is not of type int or float - - """ - if type(default) != int and type(default) != float: - raise TypeError("default value needs to be an int or float") - - builder = gtk.Builder() - builder.add_from_file(deluge.common.resource_filename( - "deluge.ui.gtkui", os.path.join("glade", "other_dialog.ui") - )) - dialog = builder.get_object("other_dialog") - dialog.set_transient_for(component.get("MainWindow").window) - dialog.set_position(gtk.WIN_POS_CENTER_ON_PARENT) - dialog.set_title("") - builder.get_object("label_header").set_markup("" + header + "") - builder.get_object("label_type").set_text(type_str) - if image_stockid: - builder.get_object("image").set_from_stock(image_stockid, gtk.ICON_SIZE_LARGE_TOOLBAR) - if image_filename: - # Hack for Windows since it doesn't support svg - if os.path.splitext(image_filename)[1] == ".svg" and (deluge.common.windows_check() or deluge.common.osx_check()): - image_filename = os.path.splitext(image_filename)[0] + "16.png" - pixbuf = gtk.gdk.pixbuf_new_from_file_at_size( - deluge.common.get_pixmap(image_filename), 32, 32) - builder.get_object("image").set_from_pixbuf(pixbuf) - - spinbutton = builder.get_object("spinbutton") - if type(default) == float: - spinbutton.set_digits(1) - - # Set default value and select text - spinbutton.set_value(default) - spinbutton.select_region(0, -1) - - value = None - response = dialog.run() - if response == gtk.RESPONSE_OK: - if type(default) == int: - value = spinbutton.get_value_as_int() - else: - value = spinbutton.get_value() - - dialog.destroy() - return value def reparent_iter(treestore, itr, parent, move_siblings=False): """ This effectively moves itr plus it's children to be a child of parent in treestore - :param treestore: gtkTreeStore, the treestore - :param itr: gtkTreeIter, the iter to move - :param parent: gtkTreeIter, the new parent for itr - :param move_siblings: bool. if True, it will move all itr's siblings to parent + Params: + treestore (gtkTreeStore): the treestore + itr (gtkTreeIter): the iter to move + parent (gtkTreeIter): the new parent for itr + move_siblings (bool): if True, it will move all itr's siblings to parent """ src = itr + def move_children(i, dest): while i: - n = treestore.append(dest, treestore.get(i, *xrange(treestore.get_n_columns()))) + n_cols = treestore.append(dest, treestore.get(i, *xrange(treestore.get_n_columns()))) to_remove = i if treestore.iter_children(i): - move_children(treestore.iter_children(i), n) + move_children(treestore.iter_children(i), n_cols) if i != src: i = treestore.iter_next(i) else: @@ -219,12 +126,15 @@ def reparent_iter(treestore, itr, parent, move_siblings=False): move_children(itr, parent) + def get_deluge_icon(): - """ - Returns the deluge icon for use in setting a dialogs icon. It will first - attempt to get the icon from the theme and will fallback to using an image + """The deluge icon for use in dialogs. + + It will first attempt to get the icon from the theme and will fallback to using an image that is distributed with the package. + Returns: + gtk.gdk.Pixbuf: the deluge icon """ if deluge.common.windows_check(): return get_logo(32) @@ -232,18 +142,19 @@ def get_deluge_icon(): try: icon_theme = gtk.icon_theme_get_default() return icon_theme.load_icon("deluge", 64, 0) - except: + except GError: return get_logo(64) + def associate_magnet_links(overwrite=False): """ Associates magnet links to Deluge. - :param overwrite: if this is True, the current setting will be overwritten - :type overwrite: bool - :returns: True if association was set - :rtype: bool + Params: + overwrite (bool): if this is True, the current setting will be overwritten + Returns: + bool: True if association was set """ if not deluge.common.windows_check(): # gconf method is only available in a GNOME environment @@ -267,10 +178,13 @@ def associate_magnet_links(overwrite=False): return False return False + def save_pickled_state_file(filename, state): """Save a file in the config directory and creates a backup - filename: Filename to be saved to config - state: The data to be pickled and written to file + + Params: + filename (str): Filename to be saved to config + state (state): The data to be pickled and written to file """ from deluge.configmanager import get_config_dir filepath = os.path.join(get_config_dir(), "gtkui_state", filename) @@ -298,10 +212,15 @@ def save_pickled_state_file(filename, state): log.info("Restoring backup of %s from: %s", filename, filepath_bak) shutil.move(filepath_bak, filepath) + def load_pickled_state_file(filename): """Loads a file from the config directory, attempting backup if original fails to load. - filename: Filename to be loaded from config - returns unpickled state + + Params: + filename (str): Filename to be loaded from config + + Returns: + state: the unpickled state """ from deluge.configmanager import get_config_dir filepath = os.path.join(get_config_dir(), "gtkui_state", filename) @@ -313,7 +232,7 @@ def load_pickled_state_file(filename): try: with open(_filepath, "rb") as _file: state = cPickle.load(_file) - except (IOError, cPickle.UnpicklingError), ex: + except (IOError, cPickle.UnpicklingError) as ex: log.warning("Unable to load %s: %s", _filepath, ex) else: log.info("Successfully loaded %s: %s", filename, _filepath) diff --git a/deluge/ui/gtkui/dialogs.py b/deluge/ui/gtkui/dialogs.py index ead22f349..c5fe9089f 100644 --- a/deluge/ui/gtkui/dialogs.py +++ b/deluge/ui/gtkui/dialogs.py @@ -31,13 +31,13 @@ # this exception statement from your version. If you delete this exception # statement from all source files in the program, then also delete it here. # - import gtk from twisted.internet import defer from deluge.ui.gtkui import common import deluge.component as component +import deluge.common class BaseDialog(gtk.Dialog): @@ -69,7 +69,14 @@ class BaseDialog(gtk.Dialog): self.set_default_size(200, 100) hbox = gtk.HBox(spacing=5) image = gtk.Image() - image.set_from_stock(icon, gtk.ICON_SIZE_DIALOG) + if not gtk.stock_lookup(icon) and (icon.endswith(".svg") or icon.endswith(".png")): + # Hack for Windows since it doesn't support svg + if icon.endswith(".svg") and (deluge.common.windows_check() or deluge.common.osx_check()): + icon = icon.rpartition(".svg")[0] + "16.png" + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(deluge.common.get_pixmap(icon), 32, 32) + image.set_from_pixbuf(pixbuf) + else: + image.set_from_stock(icon, gtk.ICON_SIZE_DIALOG) image.set_alignment(0.5, 0.0) hbox.pack_start(image, False, False) vbox = gtk.VBox(spacing=5) @@ -105,6 +112,7 @@ class BaseDialog(gtk.Dialog): self.show() return self.deferred + class YesNoDialog(BaseDialog): """ Displays a dialog asking the user to select Yes or No to a question. @@ -122,9 +130,10 @@ class YesNoDialog(BaseDialog): header, text, gtk.STOCK_DIALOG_QUESTION, - (gtk.STOCK_YES, gtk.RESPONSE_YES, gtk.STOCK_NO, gtk.RESPONSE_NO), + (gtk.STOCK_NO, gtk.RESPONSE_NO, gtk.STOCK_YES, gtk.RESPONSE_YES), parent) + class InformationDialog(BaseDialog): """ Displays an information dialog. @@ -144,6 +153,7 @@ class InformationDialog(BaseDialog): (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE), parent) + class ErrorDialog(BaseDialog): """ Displays an error dialog with optional details text for more information. @@ -193,6 +203,7 @@ class ErrorDialog(BaseDialog): self.vbox.pack_start(sw) self.vbox.show_all() + class AuthenticationDialog(BaseDialog): """ Displays a dialog with entry fields asking for username and password. @@ -213,7 +224,7 @@ class AuthenticationDialog(BaseDialog): table = gtk.Table(2, 2, False) self.username_label = gtk.Label() - self.username_label.set_markup(_("Username:")) + self.username_label.set_markup("" + _("Username:") + "") self.username_label.set_alignment(1.0, 0.5) self.username_label.set_padding(5, 5) self.username_entry = gtk.Entry() @@ -221,7 +232,7 @@ class AuthenticationDialog(BaseDialog): table.attach(self.username_entry, 1, 2, 0, 1) self.password_label = gtk.Label() - self.password_label.set_markup(_("Password:")) + self.password_label.set_markup("" + _("Password:") + "") self.password_label.set_alignment(1.0, 0.5) self.password_label.set_padding(5, 5) self.password_entry = gtk.Entry() @@ -249,6 +260,7 @@ 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): @@ -273,7 +285,7 @@ class AccountDialog(BaseDialog): table = gtk.Table(2, 3, False) self.username_label = gtk.Label() - self.username_label.set_markup(_("Username:")) + self.username_label.set_markup("" + _("Username:") + "") self.username_label.set_alignment(1.0, 0.5) self.username_label.set_padding(5, 5) self.username_entry = gtk.Entry() @@ -281,7 +293,7 @@ class AccountDialog(BaseDialog): table.attach(self.username_entry, 1, 2, 0, 1) self.authlevel_label = gtk.Label() - self.authlevel_label.set_markup(_("Authentication Level:")) + self.authlevel_label.set_markup("" + _("Authentication Level:") + "") self.authlevel_label.set_alignment(1.0, 0.5) self.authlevel_label.set_padding(5, 5) @@ -289,7 +301,7 @@ class AccountDialog(BaseDialog): active_idx = None for idx, level in enumerate(levels_mapping.keys()): self.authlevel_combo.append_text(level) - if authlevel and authlevel==level: + if authlevel and authlevel == level: active_idx = idx elif not authlevel and level == 'DEFAULT': active_idx = idx @@ -301,7 +313,7 @@ class AccountDialog(BaseDialog): table.attach(self.authlevel_combo, 1, 2, 1, 2) self.password_label = gtk.Label() - self.password_label.set_markup(_("Password:")) + self.password_label.set_markup("" + _("Password:") + "") self.password_label.set_alignment(1.0, 0.5) self.password_label.set_padding(5, 5) self.password_entry = gtk.Entry() @@ -331,3 +343,63 @@ class AccountDialog(BaseDialog): combobox = self.authlevel_combo level = combobox.get_model()[combobox.get_active()][0] return level + + +class OtherDialog(BaseDialog): + """ + Displays a dialog with a spinner for setting a value. + + Returns: + int or float: + """ + def __init__(self, header, text="", unit_text="", icon=None, default=0, parent=None): + self.value_type = type(default) + if self.value_type not in (int, float): + raise TypeError("default value needs to be an int or float") + + if not icon: + icon = gtk.STOCK_DIALOG_INFO + + super(OtherDialog, self).__init__( + header, + text, + icon, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_APPLY, gtk.RESPONSE_OK), + parent) + + hbox = gtk.HBox(spacing=5) + alignment_spacer = gtk.Alignment() + hbox.pack_start(alignment_spacer) + alignment_spin = gtk.Alignment(1, 0.5, 1, 1) + adjustment_spin = gtk.Adjustment(value=-1, lower=-1, upper=2097151, step_incr=1, page_incr=10) + self.spinbutton = gtk.SpinButton(adjustment_spin) + self.spinbutton.set_value(default) + self.spinbutton.select_region(0, -1) + self.spinbutton.set_width_chars(6) + self.spinbutton.set_alignment(1) + self.spinbutton.set_max_length(6) + if self.value_type is float: + self.spinbutton.set_digits(1) + alignment_spin.add(self.spinbutton) + hbox.pack_start(alignment_spin, expand=False) + label_type = gtk.Label() + label_type.set_text(unit_text) + label_type.set_alignment(0.0, 0.5) + hbox.pack_start(label_type) + + self.vbox.pack_start(hbox, False, False, padding=5) + self.vbox.show_all() + + def _on_delete_event(self, widget, event): + self.deferred.callback(None) + self.destroy() + + def _on_response(self, widget, response): + value = None + if response == gtk.RESPONSE_OK: + if self.value_type is int: + value = self.spinbutton.get_value_as_int() + else: + value = self.spinbutton.get_value() + self.deferred.callback(value) + self.destroy() diff --git a/deluge/ui/gtkui/glade/torrent_menu.options.ui b/deluge/ui/gtkui/glade/torrent_menu.options.ui index 20d800098..204467912 100644 --- a/deluge/ui/gtkui/glade/torrent_menu.options.ui +++ b/deluge/ui/gtkui/glade/torrent_menu.options.ui @@ -60,6 +60,15 @@ False + + + True + False + False + Stop seed at _ratio + True + + True diff --git a/deluge/ui/gtkui/menubar.py b/deluge/ui/gtkui/menubar.py index f36666d3b..6e2efe314 100644 --- a/deluge/ui/gtkui/menubar.py +++ b/deluge/ui/gtkui/menubar.py @@ -44,13 +44,13 @@ import deluge.error import deluge.component as component from deluge.ui.client import client import deluge.common -import common import dialogs from deluge.configmanager import ConfigManager from deluge.ui.gtkui.path_chooser import PathChooser log = logging.getLogger(__name__) + class MenuBar(component.Component): def __init__(self): @@ -85,7 +85,7 @@ class MenuBar(component.Component): self.builder.get_object("upload-limit-image").set_from_file(deluge.common.get_pixmap("seeding16.png")) for menuitem in ("menuitem_down_speed", "menuitem_up_speed", - "menuitem_max_connections", "menuitem_upload_slots"): + "menuitem_max_connections", "menuitem_upload_slots"): submenu = gtk.Menu() item = gtk.MenuItem(_("Set Unlimited")) item.set_name(menuitem) @@ -108,6 +108,17 @@ class MenuBar(component.Component): submenu.show_all() self.builder.get_object("menuitem_auto_managed").set_submenu(submenu) + submenu = gtk.Menu() + item = gtk.MenuItem(_("Disable")) + item.connect("activate", self.on_menuitem_set_stop_seed_at_ratio_disable) + submenu.append(item) + item = gtk.MenuItem(_("Enable...")) + item.set_name("menuitem_stop_seed_at_ratio") + item.connect("activate", self.on_menuitem_set_other) + submenu.append(item) + submenu.show_all() + self.builder.get_object("menuitem_stop_seed_at_ratio").set_submenu(submenu) + self.torrentmenu = self.builder.get_object("torrent_menu") self.menu_torrent = self.main_builder.get_object("menu_torrent") @@ -121,7 +132,6 @@ class MenuBar(component.Component): self.main_builder.get_object("sidebar_show_zero").set_active(self.config["sidebar_show_zero"]) self.main_builder.get_object("sidebar_show_trackers").set_active(self.config["sidebar_show_trackers"]) - ### Connect main window Signals ### component.get("MainWindow").connect_signals({ ## File Menu @@ -144,8 +154,8 @@ class MenuBar(component.Component): "on_menuitem_faq_activate": self.on_menuitem_faq_activate, "on_menuitem_community_activate": self.on_menuitem_community_activate, "on_menuitem_about_activate": self.on_menuitem_about_activate, - "on_menuitem_sidebar_zero_toggled":self.on_menuitem_sidebar_zero_toggled, - "on_menuitem_sidebar_trackers_toggled":self.on_menuitem_sidebar_trackers_toggled + "on_menuitem_sidebar_zero_toggled": self.on_menuitem_sidebar_zero_toggled, + "on_menuitem_sidebar_trackers_toggled": self.on_menuitem_sidebar_trackers_toggled }) # Connect menubar signals @@ -204,8 +214,7 @@ class MenuBar(component.Component): if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN: # Get known accounts to allow changing ownership client.core.get_known_accounts().addCallback( - self._on_known_accounts).addErrback(self._on_known_accounts_fail - ) + self._on_known_accounts).addErrback(self._on_known_accounts_fail) def stop(self): log.debug("MenuBar stopping") @@ -315,6 +324,7 @@ class MenuBar(component.Component): def on_menuitem_open_folder_activate(self, data=None): log.debug("on_menuitem_open_folder") + def _on_torrent_status(status): deluge.common.open_file(status["save_path"]) for torrent_id in component.get("TorrentView").get_selected_torrents(): @@ -422,36 +432,54 @@ class MenuBar(component.Component): def on_menuitem_set_other(self, widget): log.debug("widget.name: %s", widget.name) - funcs = { - "menuitem_down_speed": client.core.set_torrent_max_download_speed, - "menuitem_up_speed": client.core.set_torrent_max_upload_speed, - "menuitem_max_connections": client.core.set_torrent_max_connections, - "menuitem_upload_slots": client.core.set_torrent_max_upload_slots - } - # 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 - ) + status_map = { + "menuitem_down_speed": ["max_download_speed", "max_download_speed"], + "menuitem_up_speed": ["max_upload_speed", "max_upload_speed"], + "menuitem_max_connections": ["max_connections", "max_connections_global"], + "menuitem_upload_slots": ["max_upload_slots", "max_upload_slots_global"], + "menuitem_stop_seed_at_ratio": ["stop_ratio", "stop_seed_ratio"] } - # Show the other dialog - value = common.show_other_dialog(*other_dialog_info[widget.name]) - if value and widget.name in funcs: - for torrent in component.get("TorrentView").get_selected_torrents(): - funcs[widget.name](torrent, value) + other_dialog_info = { + "menuitem_down_speed": [_("Download Speed Limit"), _("Set the maximum download speed"), + _("KiB/s"), "downloading.svg"], + "menuitem_up_speed": [_("Upload Speed Limit"), _("Set the maximum upload speed"), + _("KiB/s"), "seeding.svg"], + "menuitem_max_connections": [_("Incoming Connections"), _("Set the maximum incoming connections"), + "", gtk.STOCK_NETWORK], + "menuitem_upload_slots": [_("Peer Upload Slots"), _("Set the maximum upload slots"), + "", gtk.STOCK_SORT_ASCENDING], + "menuitem_stop_seed_at_ratio": [_("Stop Seed At Ratio"), "Stop torrent seeding at ratio", "", None] + } + + core_key = status_map[widget.name][0] + core_key_global = status_map[widget.name][1] + + def _on_torrent_status(status): + other_dialog = other_dialog_info[widget.name] + # Add the default using status value + if status: + other_dialog.append(status[core_key_global]) + + def set_value(value): + if value is not None: + if value == 0: + value += -1 + options = {core_key: value} + if core_key == "stop_ratio": + options["stop_at_ratio"] = True + client.core.set_torrent_options(torrent_ids, options) + + dialog = dialogs.OtherDialog(*other_dialog) + dialog.run().addCallback(set_value) + + torrent_ids = component.get("TorrentView").get_selected_torrents() + if len(torrent_ids) == 1: + core_key_global = core_key + d = component.get("SessionProxy").get_torrent_status(torrent_ids[0], {core_key}) + else: + d = client.core.get_config_values([core_key_global]) + d.addCallback(_on_torrent_status) def on_menuitem_set_automanaged_on(self, widget): for torrent in component.get("TorrentView").get_selected_torrents(): @@ -461,6 +489,10 @@ class MenuBar(component.Component): for torrent in component.get("TorrentView").get_selected_torrents(): client.core.set_torrent_auto_managed(torrent, False) + def on_menuitem_set_stop_seed_at_ratio_disable(self, widget): + client.core.set_torrent_options(component.get("TorrentView").get_selected_torrents(), + {"stop_at_ratio": False}) + def on_menuitem_sidebar_zero_toggled(self, widget): self.config["sidebar_show_zero"] = widget.get_active() component.get("FilterTreeView").update() diff --git a/deluge/ui/gtkui/statusbar.py b/deluge/ui/gtkui/statusbar.py index 24c3aedd3..cdd1a596e 100644 --- a/deluge/ui/gtkui/statusbar.py +++ b/deluge/ui/gtkui/statusbar.py @@ -41,7 +41,8 @@ import logging from deluge.ui.client import client import deluge.component as component import deluge.common -import common +from deluge.ui.gtkui import common +from deluge.ui.gtkui import dialogs from deluge.configmanager import ConfigManager log = logging.getLogger(__name__) @@ -123,7 +124,7 @@ class StatusBar(component.Component): self.config = ConfigManager("gtkui.conf") # Status variables that are updated via callback - self.max_connections = -1 + self.max_connections_global = -1 self.num_connections = 0 self.max_download_speed = -1.0 self.download_rate = "" @@ -292,12 +293,11 @@ class StatusBar(component.Component): This is called when we receive a ConfigValueChangedEvent from the core. """ - if key in self.config_value_changed_dict.keys(): self.config_value_changed_dict[key](value) def _on_max_connections_global(self, max_connections): - self.max_connections = max_connections + self.max_connections_global = max_connections self.update_connections_label() def _on_dht(self, value): @@ -345,10 +345,10 @@ class StatusBar(component.Component): def update_connections_label(self): # Set the max connections label - if self.max_connections < 0: + if self.max_connections_global < 0: label_string = "%s" % self.num_connections else: - label_string = "%s (%s)" % (self.num_connections, self.max_connections) + label_string = "%s (%s)" % (self.num_connections, self.max_connections_global) self.connections_item.set_text(label_string) @@ -377,13 +377,47 @@ class StatusBar(component.Component): self.upload_item.set_text(label_string) def update_traffic_label(self): - label_string = "%.2f/%.2f %s" % (self.download_protocol_rate, self.upload_protocol_rate, _("KiB/s")) + label_string = "%i/%i %s" % (self.download_protocol_rate, self.upload_protocol_rate, _("KiB/s")) self.traffic_item.set_text(label_string) def update(self): # Send status request self.send_status_request() + def set_limit_value(self, widget, core_key): + """ """ + log.debug("_on_set_unlimit_other %s", core_key) + other_dialog_info = { + "max_download_speed": (_("Download Speed Limit"), _("Set the maximum download speed"), + _("KiB/s"), "downloading.svg", self.max_download_speed), + "max_upload_speed": (_("Upload Speed Limit"), _("Set the maximum upload speed"), + _("KiB/s"), "seeding.svg", self.max_upload_speed), + "max_connections_global": (_("Incoming Connections"), _("Set the maximum incoming connections"), + "", gtk.STOCK_NETWORK, self.max_connections_global) + } + + def set_value(value): + log.debug('value: %s', value) + if value is None: + return + elif value == 0: + value = -1 + # Set the config in the core + if value != getattr(self, core_key): + client.core.set_config({core_key: value}) + + if widget.get_name() == "unlimited": + set_value(-1) + elif widget.get_name() == "other": + def dialog_finished(response_id): + if response_id == gtk.RESPONSE_OK: + set_value(dialog.get_value()) + dialog = dialogs.OtherDialog(*other_dialog_info[core_key]) + dialog.run().addCallback(set_value) + else: + value = widget.get_children()[0].get_text().split(" ")[0] + set_value(value) + def _on_download_item_clicked(self, widget, event): menu = common.build_menu_radio_list( self.config["tray_download_speed_list"], @@ -395,22 +429,7 @@ class StatusBar(component.Component): def _on_set_download_speed(self, widget): log.debug("_on_set_download_speed") - - if widget.get_name() == "unlimited": - value = -1 - elif widget.get_name() == "other": - value = common.show_other_dialog( - _("Set Maximum Download Speed"), _("KiB/s"), None, "downloading.svg", self.max_download_speed) - if value is None: - return - else: - value = float(widget.get_children()[0].get_text().split(" ")[0]) - - log.debug("value: %s", value) - - # Set the config in the core - if value != self.max_download_speed: - client.core.set_config({"max_download_speed": value}) + self.set_limit_value(widget, "max_download_speed") def _on_upload_item_clicked(self, widget, event): menu = common.build_menu_radio_list( @@ -423,49 +442,19 @@ class StatusBar(component.Component): def _on_set_upload_speed(self, widget): log.debug("_on_set_upload_speed") - - if widget.get_name() == "unlimited": - value = -1 - elif widget.get_name() == "other": - value = common.show_other_dialog( - _("Set Maximum Upload Speed"), _("KiB/s"), None, "seeding.svg", self.max_upload_speed) - if value is None: - return - else: - value = float(widget.get_children()[0].get_text().split(" ")[0]) - - log.debug("value: %s", value) - - # Set the config in the core - if value != self.max_upload_speed: - client.core.set_config({"max_upload_speed": value}) + self.set_limit_value(widget, "max_upload_speed") def _on_connection_item_clicked(self, widget, event): menu = common.build_menu_radio_list( self.config["connection_limit_list"], self._on_set_connection_limit, - self.max_connections, show_notset=True, show_other=True) + self.max_connections_global, show_notset=True, show_other=True) menu.show_all() menu.popup(None, None, None, event.button, event.time) def _on_set_connection_limit(self, widget): log.debug("_on_set_connection_limit") - - if widget.get_name() == "unlimited": - value = -1 - elif widget.get_name() == "other": - value = common.show_other_dialog( - _("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, self.max_connections) - if value is None: - return - else: - value = int(widget.get_children()[0].get_text().split(" ")[0]) - - log.debug("value: %s", value) - - # Set the config in the core - if value != self.max_connections: - client.core.set_config({"max_connections_global": value}) + self.set_limit_value(widget, "max_connections_global") def _on_health_icon_clicked(self, widget, event): component.get("Preferences").show("Network") diff --git a/deluge/ui/gtkui/systemtray.py b/deluge/ui/gtkui/systemtray.py index 33ad514fb..01065e4d6 100644 --- a/deluge/ui/gtkui/systemtray.py +++ b/deluge/ui/gtkui/systemtray.py @@ -46,10 +46,12 @@ import deluge.component as component from deluge.ui.client import client import deluge.common from deluge.configmanager import ConfigManager +from deluge.ui.gtkui import dialogs import common log = logging.getLogger(__name__) + class SystemTray(component.Component): def __init__(self): component.Component.__init__(self, "SystemTray", interval=4) @@ -102,11 +104,11 @@ class SystemTray(component.Component): self.tray_menu = self.builder.get_object("tray_menu") if appindicator and self.config["enable_appindicator"]: - log.debug("Enabling the Application Indicator..") - self.indicator = appindicator.Indicator ( - "deluge", "deluge", appindicator.CATEGORY_APPLICATION_STATUS) + log.debug("Enabling the Application Indicator...") + self.indicator = appindicator.Indicator("deluge", "deluge", + appindicator.CATEGORY_APPLICATION_STATUS) try: - self.indicator.set_property ("title", _("Deluge")) + self.indicator.set_property("title", _("Deluge")) except TypeError: # Catch 'title' property error for previous appindicator versions pass @@ -139,9 +141,9 @@ class SystemTray(component.Component): self.tray.connect("popup-menu", self.on_tray_popup) self.builder.get_object("download-limit-image").set_from_file( - deluge.common.get_pixmap("downloading16.png")) + deluge.common.get_pixmap("downloading16.png")) self.builder.get_object("upload-limit-image").set_from_file( - deluge.common.get_pixmap("seeding16.png")) + deluge.common.get_pixmap("seeding16.png")) client.register_event_handler("ConfigValueChangedEvent", self.config_value_changed) if client.connected(): @@ -245,9 +247,10 @@ class SystemTray(component.Component): else: max_upload_speed = "%s %s" % (max_upload_speed, _("KiB/s")) - msg = '%s\n%s: %s (%s)\n%s: %s (%s)' % (\ - _("Deluge"), _("Down"), self.download_rate, \ - max_download_speed, _("Up"), self.upload_rate, max_upload_speed) + msg = '%s\n%s: %s (%s)\n%s: %s (%s)' % ( + _("Deluge"), _("Down"), self.download_rate, + max_download_speed, _("Up"), self.upload_rate, max_upload_speed + ) # Set the tooltip self.tray.set_tooltip(msg) @@ -257,15 +260,17 @@ class SystemTray(component.Component): def build_tray_bwsetsubmenu(self): # Create the Download speed list sub-menu submenu_bwdownset = common.build_menu_radio_list( - self.config["tray_download_speed_list"], self.on_tray_setbwdown, - self.max_download_speed, - _("KiB/s"), show_notset=True, show_other=True) + self.config["tray_download_speed_list"], self.on_tray_setbwdown, + self.max_download_speed, + _("KiB/s"), show_notset=True, show_other=True + ) # Create the Upload speed list sub-menu submenu_bwupset = common.build_menu_radio_list( - self.config["tray_upload_speed_list"], self.on_tray_setbwup, - self.max_upload_speed, - _("KiB/s"), show_notset=True, show_other=True) + self.config["tray_upload_speed_list"], self.on_tray_setbwup, + self.max_upload_speed, + _("KiB/s"), show_notset=True, show_other=True + ) # Add the sub-menus to the tray menu self.builder.get_object("menuitem_download_limit").set_submenu( submenu_bwdownset) @@ -276,7 +281,7 @@ class SystemTray(component.Component): submenu_bwdownset.show_all() submenu_bwupset.show_all() - def disable(self,invert_app_ind_conf=False): + def disable(self, invert_app_ind_conf=False): """Disables the system tray icon or appindicator.""" try: if invert_app_ind_conf: @@ -347,8 +352,7 @@ class SystemTray(component.Component): if deluge.common.windows_check(): popup_function = None button = 0 - self.tray_menu.popup(None, None, popup_function, - button, activate_time, status_icon) + self.tray_menu.popup(None, None, popup_function, button, activate_time, status_icon) def on_menuitem_show_deluge_activate(self, menuitem): log.debug("on_menuitem_show_deluge_activate") @@ -391,16 +395,18 @@ class SystemTray(component.Component): #ignore previous radiomenuitem value if not widget.get_active(): return - self.setbwlimit(widget, _("Set Maximum Download Speed"), "max_download_speed", - "tray_download_speed_list", self.max_download_speed, "downloading.svg") + self.setbwlimit(widget, _("Download Speed Limit"), _("Set the maximum download speed"), + "max_download_speed", "tray_download_speed_list", self.max_download_speed, + "downloading.svg") def on_tray_setbwup(self, widget, data=None): if isinstance(widget, gtk.RadioMenuItem): #ignore previous radiomenuitem value if not widget.get_active(): return - self.setbwlimit(widget, _("Set Maximum Upload Speed"), "max_upload_speed", - "tray_upload_speed_list", self.max_upload_speed, "seeding.svg") + self.setbwlimit(widget, _("Upload Speed Limit"), _("Set the maximum upload speed"), + "max_upload_speed", "tray_upload_speed_list", self.max_upload_speed, + "seeding.svg") def _on_window_hide(self, widget, data=None): """_on_window_hide - update the menuitem's status""" @@ -412,20 +418,23 @@ class SystemTray(component.Component): log.debug("_on_window_show") self.builder.get_object("menuitem_show_deluge").set_active(True) - def setbwlimit(self, widget, string, core_key, ui_key, default, image): + def setbwlimit(self, widget, header, text, core_key, ui_key, default, image): """Sets the bandwidth limit based on the user selection.""" - value = widget.get_children()[0].get_text().split(" ")[0] - log.debug('setbwlimit: %s', value) - if widget.get_name() == "unlimited": - value = -1 - if widget.get_name() == "other": - value = common.show_other_dialog(string, _("KiB/s"), None, image, default) - if value == None: + def set_value(value): + log.debug('setbwlimit: %s', value) + if value is None: return elif value == 0: value = -1 - # Set the config in the core - client.core.set_config({core_key: value}) + # Set the config in the core + client.core.set_config({core_key: value}) + if widget.get_name() == "unlimited": + set_value(-1) + elif widget.get_name() == "other": + dialog = dialogs.OtherDialog(header, text, _("KiB/s"), image, default) + dialog.run().addCallback(set_value) + else: + set_value(widget.get_children()[0].get_text().split(" ")[0]) def unlock_tray(self, is_showing_dlg=[False]): try: @@ -445,8 +454,8 @@ class SystemTray(component.Component): entered_pass.set_visibility(False) self.tray_lock = gtk.Dialog(title="", parent=self.window.window, - buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, - gtk.RESPONSE_OK)) + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK)) self.tray_lock.set_default_response(gtk.RESPONSE_OK) self.tray_lock.set_has_separator(False)