diff options
Diffstat (limited to 'deluge/ui/gtk3/dialogs.py')
-rw-r--r-- | deluge/ui/gtk3/dialogs.py | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/deluge/ui/gtk3/dialogs.py b/deluge/ui/gtk3/dialogs.py new file mode 100644 index 0000000..db337d3 --- /dev/null +++ b/deluge/ui/gtk3/dialogs.py @@ -0,0 +1,498 @@ +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# 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. +# + +# pylint: disable=super-on-old-class + +from collections import namedtuple + +from gi.repository import Gtk +from twisted.internet import defer + +import deluge.component as component +from deluge.common import windows_check + +from .common import get_deluge_icon, get_pixbuf + +Account = namedtuple('Account', 'username password authlevel') + + +class BaseDialog(Gtk.Dialog): + """ + Base dialog class that should be used with all dialogs. + """ + + def __init__(self, header, text, icon, buttons, parent=None): + """ + :param header: str, the header portion of the dialog + :param text: str, the text body of the dialog + :param icon: icon name from icon theme or icon filename. + :param buttons: tuple, of icon name and responses + :param parent: gtkWindow, the parent window, if None it will default to the + MainWindow + """ + super().__init__( + title=header, + parent=parent if parent else component.get('MainWindow').window, + flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + buttons=buttons, + ) + + self.set_icon(get_deluge_icon()) + + self.connect('delete-event', self._on_delete_event) + self.connect('response', self._on_response) + + # Setup all the formatting and such to make our dialog look pretty + self.set_border_width(5) + self.set_default_size(200, 100) + hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=5) + image = Gtk.Image() + if icon.endswith('.svg') or icon.endswith('.png'): + # Hack for Windows since it doesn't support svg + if icon.endswith('.svg') and windows_check(): + icon = icon.rpartition('.svg')[0] + '16.png' + image.set_from_pixbuf(get_pixbuf(icon, 24)) + else: + image.set_from_icon_name(icon, Gtk.IconSize.LARGE_TOOLBAR) + image.set_alignment(0.5, 0.0) + hbox.pack_start(image, False, False, 0) + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, spacing=5) + tlabel = Gtk.Label(label=text) + tlabel.set_use_markup(True) + tlabel.set_line_wrap(True) + tlabel.set_alignment(0.0, 0.5) + vbox.pack_start(tlabel, False, False, 0) + hbox.pack_start(vbox, False, False, 0) + self.vbox.pack_start(hbox, False, False, 0) + self.vbox.set_spacing(5) + self.vbox.show_all() + + def _on_delete_event(self, widget, event): + self.destroy() + self.deferred.callback(Gtk.ResponseType.DELETE_EVENT) + + def _on_response(self, widget, response): + self.destroy() + self.deferred.callback(response) + + def run(self): + """ + Shows the dialog and returns a Deferred object. The deferred, when fired + will contain the response ID. + """ + self.deferred = defer.Deferred() + self.show() + return self.deferred + + +class YesNoDialog(BaseDialog): + """ + Displays a dialog asking the user to select Yes or No to a question. + + When run(), it will return either a Gtk.ResponseType.YES or a Gtk.ResponseType.NO. + + """ + + def __init__(self, header, text, parent=None): + """ + :param header: see `:class:BaseDialog` + :param text: see `:class:BaseDialog` + :param parent: see `:class:BaseDialog` + """ + super().__init__( + header, + text, + 'dialog-question', + (_('_No'), Gtk.ResponseType.NO, _('_Yes'), Gtk.ResponseType.YES), + parent, + ) + # Use the preferred size calculated from the content + self.set_default_size(-1, -1) + + +class InformationDialog(BaseDialog): + """ + Displays an information dialog. + + When run(), it will return a Gtk.ResponseType.CLOSE. + """ + + def __init__(self, header, text, parent=None): + """ + :param header: see `:class:BaseDialog` + :param text: see `:class:BaseDialog` + :param parent: see `:class:BaseDialog` + """ + super().__init__( + header, + text, + 'dialog-information', + (_('_Close'), Gtk.ResponseType.CLOSE), + parent, + ) + + +class ErrorDialog(BaseDialog): + """ + Displays an error dialog with optional details text for more information. + + When run(), it will return a Gtk.ResponseType.CLOSE. + """ + + def __init__(self, header, text, parent=None, details=None, traceback=False): + """ + :param header: see `:class:BaseDialog` + :param text: see `:class:BaseDialog` + :param parent: see `:class:BaseDialog` + :param details: extra information that will be displayed in a + scrollable textview + :type details: string + :param traceback: show the traceback information in the details area + :type traceback: bool + """ + super().__init__( + header, text, 'dialog-error', (_('_Close'), Gtk.ResponseType.CLOSE), parent + ) + + if traceback: + import sys + import traceback + + tb = sys.exc_info() + tb = traceback.format_exc(tb[2]) + if details: + details += '\n' + tb + else: + details = tb + + if details: + self.set_default_size(600, 400) + textview = Gtk.TextView() + textview.set_editable(False) + textview.get_buffer().set_text(details) + sw = Gtk.ScrolledWindow() + sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + sw.set_shadow_type(Gtk.ShadowType.IN) + sw.add(textview) + label = Gtk.Label(label=_('Details:')) + label.set_alignment(0.0, 0.5) + self.vbox.pack_start(label, False, False, 0) + self.vbox.pack_start(sw, True, True, 0) + self.vbox.show_all() + + +class AuthenticationDialog(BaseDialog): + """ + Displays a dialog with entry fields asking for username and password. + + When run(), it will return either a Gtk.ResponseType.CANCEL or a + Gtk.ResponseType.OK. + """ + + def __init__(self, err_msg='', username=None, parent=None): + """ + :param err_msg: the error message we got back from the server + :type err_msg: string + """ + super().__init__( + _('Authenticate'), + err_msg, + 'dialog-password', + (_('_Cancel'), Gtk.ResponseType.CANCEL, _('C_onnect'), Gtk.ResponseType.OK), + parent, + ) + + table = Gtk.Table(2, 2, 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.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) + self.password_entry.connect('activate', self.on_password_activate) + table.attach(self.password_label, 0, 1, 1, 2) + table.attach(self.password_entry, 1, 2, 1, 2) + + self.vbox.pack_start(table, False, False, padding=5) + self.set_focus(self.password_entry) + if username: + self.username_entry.set_text(username) + self.username_entry.set_editable(False) + self.set_focus(self.password_entry) + else: + self.set_focus(self.username_entry) + self.show_all() + + def get_username(self): + return self.username_entry.get_text() + + def get_password(self): + return self.password_entry.get_text() + + def on_password_activate(self, widget): + self.response(Gtk.ResponseType.OK) + + +class AccountDialog(BaseDialog): + def __init__( + self, + username=None, + password=None, + authlevel=None, + levels_mapping=None, + parent=None, + ): + if username: + super().__init__( + _('Edit Account'), + _('Edit existing account'), + 'dialog-information', + ( + _('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_Apply'), + Gtk.ResponseType.OK, + ), + parent, + ) + else: + super().__init__( + _('New Account'), + _('Create a new account'), + 'dialog-information', + (_('_Cancel'), Gtk.ResponseType.CANCEL, _('_Add'), Gtk.ResponseType.OK), + parent, + ) + + self.account = None + + table = Gtk.Table(2, 3, False) + username_label = Gtk.Label() + username_label.set_markup('<b>' + _('Username:') + '</b>') + username_label.set_alignment(1.0, 0.5) + username_label.set_padding(5, 5) + self.username_entry = Gtk.Entry() + table.attach(username_label, 0, 1, 0, 1) + table.attach(self.username_entry, 1, 2, 0, 1) + + authlevel_label = Gtk.Label() + authlevel_label.set_markup('<b>' + _('Authentication Level:') + '</b>') + authlevel_label.set_alignment(1.0, 0.5) + authlevel_label.set_padding(5, 5) + + # combo_box_new_text is deprecated but no other pygtk alternative. + self.authlevel_combo = Gtk.ComboBoxText() + active_idx = None + for idx, level in enumerate(levels_mapping): + self.authlevel_combo.append_text(level) + if authlevel and authlevel == level: + active_idx = idx + elif not authlevel and level == 'DEFAULT': + active_idx = idx + + if active_idx is not None: + self.authlevel_combo.set_active(active_idx) + + table.attach(authlevel_label, 0, 1, 1, 2) + table.attach(self.authlevel_combo, 1, 2, 1, 2) + + password_label = Gtk.Label() + password_label.set_markup('<b>' + _('Password:') + '</b>') + password_label.set_alignment(1.0, 0.5) + password_label.set_padding(5, 5) + self.password_entry = Gtk.Entry() + self.password_entry.set_visibility(False) + table.attach(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.vbox.show_all() + + def _on_response(self, widget, response): + if response == Gtk.ResponseType.OK: + self.account = Account( + self.username_entry.get_text(), + self.password_entry.get_text(), + self.authlevel_combo.get_active_text(), + ) + self.destroy() + self.deferred.callback(response) + + +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 = 'dialog-information' + + super().__init__( + header, + text, + icon, + (_('_Cancel'), Gtk.ResponseType.CANCEL, _('_Apply'), Gtk.ResponseType.OK), + parent, + ) + + hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=5) + alignment_spacer = Gtk.Alignment() + hbox.pack_start(alignment_spacer, True, True, 0) + alignment_spin = Gtk.Alignment(xalign=1, yalign=0.5, xscale=1, yscale=1) + adjustment_spin = Gtk.Adjustment( + value=-1, lower=-1, upper=2097151, step_increment=1, page_increment=10 + ) + self.spinbutton = Gtk.SpinButton( + adjustment=adjustment_spin, climb_rate=0, digits=0 + ) + 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, False, True, 0) + label_type = Gtk.Label() + label_type.set_text(unit_text) + label_type.set_alignment(0.0, 0.5) + hbox.pack_start(label_type, True, True, 0) + + 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.ResponseType.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() + + +class PasswordDialog(BaseDialog): + """ + Displays a dialog with an entry field asking for a password. + + When run(), it will return either a Gtk.ResponseType.CANCEL or a Gtk.ResponseType.OK. + """ + + def __init__(self, password_msg='', parent=None): + """ + :param password_msg: the error message we got back from the server + :type password_msg: string + """ + super().__init__( + header=_('Password Protected'), + text=password_msg, + icon='dialog-password', + buttons=( + _('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_OK'), + Gtk.ResponseType.OK, + ), + parent=parent, + ) + + table = Gtk.Table(1, 2, False) + 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) + self.password_entry.connect('activate', self.on_password_activate) + table.attach(self.password_label, 0, 1, 1, 2) + table.attach(self.password_entry, 1, 2, 1, 2) + + self.vbox.pack_start(table, False, False, padding=5) + self.set_focus(self.password_entry) + + self.show_all() + + def get_password(self): + return self.password_entry.get_text() + + def on_password_activate(self, widget): + self.response(Gtk.ResponseType.OK) + + +class CopyMagnetDialog(BaseDialog): + """ + Displays a dialog with a magnet URI + """ + + def __init__(self, torrent_magnet='', parent=None): + super().__init__( + header=_('Copy Magnet URI'), + text='', + icon='magnet_copy.svg', + buttons=(_('_Close'), Gtk.ResponseType.CLOSE), + parent=parent, + ) + self.copied = False + + table = Gtk.Table(1, 2, False) + self.magnet_entry = Gtk.Entry() + self.magnet_entry.set_text(torrent_magnet) + self.magnet_entry.set_editable(False) + self.magnet_entry.connect('copy-clipboard', self.on_copy_emitted) + table.attach(self.magnet_entry, 0, 1, 0, 1) + + copy_button = Gtk.Button.new_with_label(_('Copy')) + copy_button.connect('clicked', self.on_copy_clicked) + table.attach(copy_button, 1, 2, 0, 1) + + self.vbox.pack_start(table, False, False, padding=5) + self.set_focus(self.magnet_entry) + + self.show_all() + + def on_copy_clicked(self, widget): + self.magnet_entry.select_region(0, -1) + self.magnet_entry.copy_clipboard() + self.magnet_entry.set_position(0) + self.copied = True + + def on_copy_emitted(self, widget): + self.copied = True |