* Fixed black hook requiring Py3.6 to installed locally. Will now assume Py3.6+ in installed. * Added isort traceback in pre-commit flake8 hook fails * Updated versions of Black, Prettier and isort * Keep Flake8 at 3.7.9 due to E402 issue: https://gitlab.com/pycqa/flake8/-/issues/638 * New pyproject config for isort v5 with fixes for Python 2 imports. * Fixed travis config to run Python 3.6 for lint run. Replaced the virtualenv with_system_site_packages config with Travis specific Python config value so lint run doesn't attempt to append with_system_site_packages to Python 3.6 command.
458 lines
15 KiB
Python
458 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# 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 __future__ import unicode_literals
|
|
|
|
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_at_size
|
|
|
|
|
|
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(BaseDialog, self).__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_at_size(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(YesNoDialog, self).__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(InformationDialog, self).__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(ErrorDialog, self).__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(AuthenticationDialog, self).__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(AccountDialog, self).__init__(
|
|
_('Edit Account'),
|
|
_('Edit existing account'),
|
|
'dialog-information',
|
|
(
|
|
_('_Cancel'),
|
|
Gtk.ResponseType.CANCEL,
|
|
_('_Apply'),
|
|
Gtk.ResponseType.OK,
|
|
),
|
|
parent,
|
|
)
|
|
else:
|
|
super(AccountDialog, self).__init__(
|
|
_('New Account'),
|
|
_('Create a new account'),
|
|
'dialog-information',
|
|
(_('_Cancel'), Gtk.ResponseType.CANCEL, _('_Add'), Gtk.ResponseType.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)
|
|
|
|
# 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(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
|
|
|
|
|
|
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(OtherDialog, self).__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(PasswordDialog, self).__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)
|