diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
commit | 2e2851dc13d73352530dd4495c7e05603b2e520d (patch) | |
tree | 622b9cd8e5d32091c9aa9e4937b533975a40356c /deluge/ui/gtk3 | |
parent | Initial commit. (diff) | |
download | deluge-upstream/2.1.2_dev0+20240219.tar.xz deluge-upstream/2.1.2_dev0+20240219.zip |
Adding upstream version 2.1.2~dev0+20240219.upstream/2.1.2_dev0+20240219upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/ui/gtk3')
66 files changed, 30543 insertions, 0 deletions
diff --git a/deluge/ui/gtk3/__init__.py b/deluge/ui/gtk3/__init__.py new file mode 100644 index 0000000..d1b4ec5 --- /dev/null +++ b/deluge/ui/gtk3/__init__.py @@ -0,0 +1,63 @@ +# +# Copyright (C) 2007-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. +# + +import logging +from os import environ + +from deluge.ui.ui import UI + +log = logging.getLogger(__name__) +# Hide pygame community banner +environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' + + +# Keep this class in __init__.py to avoid the console having to import everything in gtkui.py +class Gtk(UI): + cmd_description = """GTK-based graphical user interface""" + + def __init__(self, *args, **kwargs): + super().__init__( + 'gtk', *args, description='Starts the Deluge GTK+ interface', **kwargs + ) + + group = self.parser.add_argument_group(_('GTK Options')) + group.add_argument( + 'torrents', + metavar='<torrent>', + nargs='*', + default=None, + help=_( + 'Add one or more torrent files, torrent URLs or magnet URIs' + ' to a currently running Deluge GTK instance' + ), + ) + + def start(self): + super().start() + import deluge.common + + from .gtkui import GtkUI + + def run(options): + try: + gtkui = GtkUI(options) + gtkui.start() + except Exception as ex: + log.exception(ex) + raise + + deluge.common.run_profiled( + run, + self.options, + output_file=self.options.profile, + do_profile=self.options.profile, + ) + + +def start(): + Gtk().start() diff --git a/deluge/ui/gtk3/aboutdialog.py b/deluge/ui/gtk3/aboutdialog.py new file mode 100644 index 0000000..fe3452b --- /dev/null +++ b/deluge/ui/gtk3/aboutdialog.py @@ -0,0 +1,854 @@ +# +# Copyright (C) 2007 Marcos Mobley ('markybob') <markybob@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. +# + +from datetime import date + +from gi.repository import Gtk + +import deluge.component as component +from deluge.common import get_version, open_url_in_browser, windows_check +from deluge.ui.client import client + +from .common import get_deluge_icon, get_pixbuf + + +class AboutDialog: + def __init__(self): + self.about = Gtk.AboutDialog() + self.about.set_transient_for(component.get('MainWindow').window) + self.about.set_position(Gtk.WindowPosition.CENTER) + self.about.set_name(_('Deluge')) + self.about.set_program_name(_('Deluge')) + if windows_check(): + + def url_hook(dialog, url): + """Url hook for Windows OS which has no default browser.""" + open_url_in_browser(url) + return True + + self.about.connect('activate-link', url_hook) + + version = get_version() + + self.about.set_copyright( + _('Copyright %(year_start)s-%(year_end)s Deluge Team') + % {'year_start': 2007, 'year_end': date.today().year} + ) + self.about.set_comments( + _('A peer-to-peer file sharing program\nutilizing the BitTorrent protocol.') + + '\n\n' + + _('Client:') + + ' %s\n' % version + ) + self.about.set_version(version) + self.about.set_authors( + [ + _('Current Developers:'), + 'Andrew Resch', + 'Damien Churchill', + 'John Garland', + 'Calum Lind', + '', + 'libtorrent (libtorrent.org):', + 'Arvid Norberg', + '', + _('Past Developers or Contributors:'), + 'Zach Tibbitts', + 'Alon Zakai', + 'Marcos Mobley', + 'Alex Dedul', + 'Sadrul Habib Chowdhury', + 'Ido Abramovich', + 'Martijn Voncken', + ] + ) + self.about.set_artists(['Andrew Wedderburn', 'Andrew Resch']) + self.about.set_translator_credits( + '\n'.join( + [ + 'Aaron Wang Shi', + 'abbigss', + 'ABCdatos', + 'Abcx', + 'Actam', + 'Adam', + 'adaminikisi', + 'adi_oporanu', + 'Adrian Goll', + 'afby', + 'Ahmades', + 'Ahmad Farghal', + 'Ahmad Gharbeia أحمد غربية', + 'akira', + 'Aki Sivula', + 'Alan Pepelko', + 'Alberto', + 'Alberto Ferrer', + 'alcatr4z', + 'AlckO', + 'Aleksej Korgenkov', + 'Alessio Treglia', + 'Alexander Ilyashov', + 'Alexander Matveev', + 'Alexander Saltykov', + 'Alexander Taubenkorb', + 'Alexander Telenga', + 'Alexander Yurtsev', + 'Alexandre Martani', + 'Alexandre Rosenfeld', + 'Alexandre Sapata Carbonell', + 'Alexey Osipov', + 'Alin Claudiu Radut', + 'allah', + 'AlSim', + 'Alvaro Carrillanca P.', + 'A.Matveev', + 'Andras Hipsag', + 'András Kárász', + 'Andrea Ratto', + 'Andreas Johansson', + 'Andreas Str', + 'André F. Oliveira', + 'AndreiF', + 'andrewh', + 'Angel Guzman Maeso', + 'Aníbal Deboni Neto', + 'animarval', + 'Antonio Cono', + 'antoniojreyes', + 'Anton Shestakov', + 'Anton Yakutovich', + 'antou', + 'Arkadiusz Kalinowski', + 'Artin', + 'artir', + 'Astur', + 'Athanasios Lefteris', + 'Athmane MOKRAOUI (ButterflyOfFire)', + 'Augusta Carla Klug', + 'Avoledo Marco', + 'axaard', + 'AxelRafn', + 'Axezium', + 'Ayont', + 'b3rx', + 'Bae Taegil', + 'Bajusz Tamás', + "Balaam's Miracle", + 'Ballestein', + 'Bent Ole Fosse', + 'berto89', + 'bigx', + 'Bjorn Inge Berg', + 'blackbird', + 'Blackeyed', + 'blackmx', + 'BlueSky', + 'Blutheo', + 'bmhm', + 'bob00work', + 'boenki', + 'Bogdan Bădic-Spătariu', + 'bonpu', + 'Boone', + 'boss01', + 'Branislav Jovanović', + 'bronze', + 'brownie', + 'Brus46', + 'bumper', + 'butely', + 'BXCracer', + 'c0nfidencal', + 'Can Kaya', + 'Carlos Alexandro Becker', + 'cassianoleal', + 'Cédric.h', + 'César Rubén', + 'chaoswizard', + 'Chen Tao', + 'chicha', + 'Chien Cheng Wei', + 'Christian Kopac', + 'Christian Widell', + 'Christoffer Brodd-Reijer', + 'christooss', + 'CityAceE', + 'Clopy', + 'Clusty', + 'cnu', + 'Commandant', + 'Constantinos Koniaris', + 'Coolmax', + 'cosmix', + 'Costin Chirvasuta', + 'CoVaLiDiTy', + 'cow_2001', + 'Crispin Kirchner', + 'crom', + 'Cruster', + 'Cybolic', + 'Dan Bishop', + 'Danek', + 'Dani', + 'Daniel Demarco', + 'Daniel Ferreira', + 'Daniel Frank', + 'Daniel Holm', + 'Daniel Høyer Iversen', + 'Daniel Marynicz', + 'Daniel Nylander', + 'Daniel Patriche', + 'Daniel Schildt', + 'Daniil Sorokin', + 'Dante Díaz', + 'Daria Michalska', + 'DarkenCZ', + 'Darren', + 'Daspah', + 'David Eurenius', + 'davidhjelm', + 'David Machakhelidze', + 'Dawid Dziurdzia', + 'Daya Adianto ', + 'dcruz', + 'Deady', + 'Dereck Wonnacott', + 'Devgru', + 'Devid Antonio Filoni' 'DevilDogTG', + 'di0rz`', + 'Dialecti Valsamou', + 'Diego Medeiros', + 'Dkzoffy', + 'Dmitrij D. Czarkoff', + 'Dmitriy Geels', + 'Dmitry Olyenyov', + 'Dominik Kozaczko', + 'Dominik Lübben', + 'doomster', + 'Dorota Król', + 'Doyen Philippe', + 'Dread Knight', + 'DreamSonic', + 'duan', + 'Duong Thanh An', + 'DvoglavaZver', + 'dwori', + 'dylansmrjones', + 'Ebuntor', + 'Edgar Alejandro Jarquin Flores', + 'Eetu', + 'ekerazha', + 'Elias Julkunen', + 'elparia', + 'Emberke', + 'Emiliano Goday Caneda', + 'EndelWar', + 'eng.essam', + 'enubuntu', + 'ercangun', + 'Erdal Ronahi', + 'ergin üresin', + 'Eric', + 'Éric Lassauge', + 'Erlend Finvåg', + 'Errdil', + 'ethan shalev', + 'Evgeni Spasov', + 'ezekielnin', + 'Fabian Ordelmans', + 'Fabio Mazanatti', + 'Fábio Nogueira', + 'FaCuZ', + 'Felipe Lerena', + 'Fernando Pereira', + 'fjetland', + 'Florian Schäfer', + 'FoBoS', + 'Folke', + 'Force', + 'fosk', + 'fragarray', + 'freddeg', + 'Frédéric Perrin', + 'Fredrik Kilegran', + 'FreeAtMind', + 'Fulvio Ciucci', + 'Gabor Kelemen', + 'Galatsanos Panagiotis', + 'Gaussian', + 'gdevitis', + 'Georg Brzyk', + 'George Dumitrescu', + 'Georgi Arabadjiev', + 'Georg Sieber', + 'Gerd Radecke', + 'Germán Heusdens', + 'Gianni Vialetto', + 'Gigih Aji Ibrahim', + 'Giorgio Wicklein', + 'Giovanni Rapagnani', + 'Giuseppe', + 'gl', + 'glen', + 'granjerox', + 'Green Fish', + 'greentea', + 'Greyhound', + 'G. U.', + 'Guillaume BENOIT', + 'Guillaume Pelletier', + 'Gustavo Henrique Klug', + 'gutocarvalho', + 'Guybrush88', + 'Hans Rødtang', + 'HardDisk', + 'Hargas Gábor', + 'Heitor Thury Barreiros Barbosa', + 'helios91940', + 'helix84', + 'Helton Rodrigues', + 'Hendrik Luup', + 'Henrique Ferreiro', + 'Henry Goury-Laffont', + 'Hezy Amiel', + 'hidro', + 'hoball', + 'hokten', + 'Holmsss', + 'hristo.num', + 'Hubert Życiński', + 'Hyo', + 'Iarwain', + 'ibe', + 'ibear', + 'Id2ndR', + 'Igor Zubarev', + 'IKON (Ion)', + 'imen', + 'Ionuț Jula', + 'Isabelle STEVANT', + 'István Nyitrai', + 'Ivan Petrovic', + 'Ivan Prignano', + 'IvaSerge', + 'jackmc', + 'Jacks0nxD', + 'Jack Shen', + 'Jacky Yeung', + 'Jacques Stadler', + 'Janek Thomaschewski', + 'Jan Kaláb', + 'Jan Niklas Hasse', + 'Jasper Groenewegen', + 'Javi Rodríguez', + 'Jayasimha (ಜಯಸಿಂಹ)', + 'jeannich', + 'Jeff Bailes', + 'Jesse Zilstorff', + 'Joan Duran', + 'João Santos', + 'Joar Bagge', + 'Joe Anderson', + 'Joel Calado', + 'Johan Linde', + 'John Garland', + 'Jojan', + 'jollyr0ger', + 'Jonas Bo Grimsgaard', + 'Jonas Granqvist', + 'Jonas Slivka', + 'Jonathan Zeppettini', + 'Jørgen', + 'Jørgen Tellnes', + 'josé', + 'José Geraldo Gouvêa', + 'José Iván León Islas', + 'José Lou C.', + 'Jose Sun', + 'Jr.', + 'Jukka Kauppinen', + 'Julián Alarcón', + 'julietgolf', + 'Jusic', + 'Justzupi', + 'Kaarel', + 'Kai Thomsen', + 'Kalman Tarnay', + 'Kamil Páral', + 'Kane_F', + 'kaotiks@gmail.com', + 'Kateikyoushii', + 'kaxhinaz', + 'Kazuhiro NISHIYAMA', + 'Kerberos', + 'Keresztes Ákos', + 'kevintyk', + 'kiersie', + 'Kimbo^', + 'Kim Lübbe', + 'kitzOgen', + 'Kjetil Rydland', + 'kluon', + 'kmikz', + 'Knedlyk', + 'koleoptero', + 'Kőrösi Krisztián', + 'Kouta', + 'Krakatos', + 'Krešo Kunjas', + 'kripken', + 'Kristaps', + 'Kristian Øllegaard', + 'Kristoffer Egil Bonarjee', + 'Krzysztof Janowski', + 'Krzysztof Zawada', + 'Larry Wei Liu', + 'laughterwym', + 'Laur Mõtus', + 'lazka', + 'leandrud', + 'lê bình', + 'Le Coz Florent', + 'Leo', + 'liorda', + 'LKRaider', + 'LoLo_SaG', + 'Long Tran', + 'Lorenz', + 'Low Kian Seong', + 'Luca Andrea Rossi', + 'Luca Ferretti', + 'Lucky LIX', + 'Luis Gomes', + 'Luis Reis', + 'Łukasz Wyszyński', + 'luojie-dune', + 'maaark', + 'Maciej Chojnacki', + 'Maciej Meller', + 'Mads Peter Rommedahl', + 'Major Kong', + 'Malaki', + 'malde', + 'Malte Lenz', + 'Mantas Kriaučiūnas', + 'Mara Sorella', + 'Marcin', + 'Marcin Falkiewicz', + 'marcobra', + 'Marco da Silva', + 'Marco de Moulin', + 'Marco Rodrigues', + 'Marcos', + 'Marcos Escalier', + 'Marcos Mobley', + 'Marcus Ekstrom', + 'Marek Dębowski', + 'Mário Buči', + 'Mario Munda', + 'Marius Andersen', + 'Marius Hudea', + 'Marius Mihai', + 'Mariusz Cielecki', + 'Mark Krapivner', + 'marko-markovic', + 'Markus Brummer', + 'Markus Sutter', + 'Martin', + 'Martin Dybdal', + 'Martin Iglesias', + 'Martin Lettner', + 'Martin Pihl', + 'Masoud Kalali', + 'mat02', + 'Matej Urbančič', + 'Mathias-K', + 'Mathieu Arès', + 'Mathieu D. (MatToufoutu)', + 'Mathijs', + 'Matrik', + 'Matteo Renzulli', + 'Matteo Settenvini', + 'Matthew Gadd', + 'Matthias Benkard', + 'Matthias Mailänder', + 'Mattias Ohlsson', + 'Mauro de Carvalho', + 'Max Molchanov', + 'Me', + 'MercuryCC', + 'Mert Bozkurt', + 'Mert Dirik', + 'MFX', + 'mhietar', + 'mibtha', + 'Michael Budde', + 'Michael Kaliszka', + 'Michalis Makaronides', + 'Michał Tokarczyk', + 'Miguel Pires da Rosa', + 'Mihai Capotă', + 'Miika Metsälä', + 'Mikael Fernblad', + 'Mike Sierra', + 'mikhalek', + 'Milan Prvulović', + 'Milo Casagrande', + 'Mindaugas', + 'Miroslav Matejaš', + 'misel', + 'mithras', + 'Mitja Pagon', + 'M.Kitchen', + 'Mohamed Magdy', + 'moonkey', + 'MrBlonde', + 'muczy', + 'Münir Ekinci', + 'Mustafa Temizel', + 'mvoncken', + 'Mytonn', + 'NagyMarton', + 'neaion', + 'Neil Lin', + 'Nemo', + 'Nerijus Arlauskas', + 'Nicklas Larsson', + 'Nicolaj Wyke', + 'Nicola Piovesan', + 'Nicolas Sabatier', + 'Nicolas Velin', + 'Nightfall', + 'NiKoB', + 'Nikolai M. Riabov', + 'Niko_Thien', + 'niska', + 'Nithir', + 'noisemonkey', + 'nomemohes', + 'nosense', + 'null', + 'Nuno Estêvão', + 'Nuno Santos', + 'nxxs', + 'nyo', + 'obo', + 'Ojan', + 'Olav Andreas Lindekleiv', + 'oldbeggar', + 'Olivier FAURAX', + 'orphe', + 'osantana', + 'Osman Tosun', + 'OssiR', + 'otypoks', + 'ounn', + 'Oz123', + 'Özgür BASKIN', + 'Pablo Carmona A.', + 'Pablo Ledesma', + 'Pablo Navarro Castillo', + 'Paco Molinero', + 'Pål-Eivind Johnsen', + 'pano', + 'Paolo Naldini', + 'Paracelsus', + 'Patryk13_03', + 'Patryk Skorupa', + 'PattogoTehen', + 'Paul Lange', + 'Pavcio', + 'Paweł Wysocki', + 'Pedro Brites Moita', + 'Pedro Clemente Pereira Neto', + 'Pekka "PEXI" Niemistö', + 'Penegal', + 'Penzo', + 'perdido', + 'Peter Kotrcka', + 'Peter Skov', + 'Peter Van den Bosch', + 'Petter Eklund', + 'Petter Viklund', + 'phatsphere', + 'Phenomen', + 'Philipi', + 'Philippides Homer', + 'phoenix', + 'pidi', + 'Pierre Quillery', + 'Pierre Rudloff', + 'Pierre Slamich', + 'Pietrao', + 'Piotr Strębski', + 'Piotr Wicijowski', + 'Pittmann Tamás', + 'Playmolas', + 'Prescott', + 'Prescott_SK', + 'pronull', + 'Przemysław Kulczycki', + 'Pumy', + 'pushpika', + 'PY', + 'qubicllj', + 'r21vo', + 'Rafał Barański', + 'rainofchaos', + 'Rajbir', + 'ras0ir', + 'Rat', + 'rd1381', + 'Renato', + 'Rene Hennig', + 'Rene Pärts', + 'Ricardo Duarte', + 'Richard', + 'Robert Hrovat', + 'Roberth Sjonøy', + 'Robert Lundmark', + 'Robin Jakobsson', + 'Robin Kåveland', + 'Rodrigo Donado', + 'Roel Groeneveld', + 'rohmaru', + 'Rolf Christensen', + 'Rolf Leggewie', + 'Roni Kantis', + 'Ronmi', + 'Rostislav Raykov', + 'royto', + 'RuiAmaro', + 'Rui Araújo', + 'Rui Moura', + 'Rune Svendsen', + 'Rusna', + 'Rytis', + 'Sabirov Mikhail', + 'salseeg', + 'Sami Koskinen', + 'Samir van de Sand', + 'Samuel Arroyo Acuña', + 'Samuel R. C. Vale', + 'Sanel', + 'Santi', + 'Santi Martínez Cantelli', + 'Sardan', + 'Sargate Kanogan', + 'Sarmad Jari', + 'Saša Bodiroža', + 'sat0shi', + 'Saulius Pranckevičius', + 'Savvas Radevic', + 'Sebastian Krauß', + 'Sebastián Porta', + 'Sedir', + 'Sefa Denizoğlu', + 'sekolands', + 'Selim Suerkan', + 'semsomi', + 'Sergii Golovatiuk', + 'setarcos', + 'Sheki', + 'Shironeko', + 'Shlomil', + 'silfiriel', + 'Simone Tolotti', + 'Simone Vendemia', + 'sirkubador', + 'Sławomir Więch', + 'slip', + 'slyon', + 'smoke', + 'Sonja', + 'spectral', + 'spin_555', + 'spitf1r3', + 'Spiziuz', + 'Spyros Theodoritsis', + 'SqUe', + 'Squigly', + 'srtck', + 'Stefan Horning', + 'Stefano Maggiolo', + 'Stefano Roberto Soleti', + 'steinberger', + 'Stéphane Travostino', + 'Stephan Klein', + 'Steven De Winter', + 'Stevie', + 'Stian24', + 'stylius', + 'Sukarn Maini', + 'Sunjae Park', + 'Susana Pereira', + 'szymon siglowy', + 'takercena', + 'TAS', + 'Taygeto', + 'temy4', + 'texxxxxx', + 'thamood', + 'Thanos Chatziathanassiou', + 'Tharawut Paripaiboon', + 'Theodoor', + 'Théophane Anestis', + 'Thor Marius K. Høgås', + 'Tiago Silva', + 'Tiago Sousa', + 'Tikkel', + 'tim__b', + 'Tim Bordemann', + 'Tim Fuchs', + 'Tim Kornhammar', + 'Timo', + 'Timo Jyrinki', + 'Timothy Babych', + 'TitkosRejtozo', + 'Tom', + 'Tomas Gustavsson', + 'Tomas Valentukevičius', + 'Tomasz Dominikowski', + 'Tomislav Plavčić', + 'Tom Mannerhagen', + 'Tommy Mikkelsen', + 'Tom Verdaat', + 'Tony Manco', + 'Tor Erling H. Opsahl', + 'Toudi', + 'tqm_z', + 'Trapanator', + 'Tribaal', + 'Triton', + 'TuniX12', + 'Tuomo Sipola', + 'turbojugend_gr', + 'Turtle.net', + 'twilight', + 'tymmej', + 'Ulrik', + 'Umarzuki Mochlis', + 'unikob', + 'Vadim Gusev', + 'Vagi', + 'Valentin Bora', + 'Valmantas Palikša', + 'VASKITTU', + 'Vassilis Skoullis', + 'vetal17', + 'vicedo', + 'viki', + 'villads hamann', + 'Vincent Garibal', + 'Vincent Ortalda', + 'vinchi007', + 'Vinícius de Figueiredo Silva', + 'Vinzenz Vietzke', + 'virtoo', + 'virtual_spirit', + 'Vitor Caike', + 'Vitor Lamas Gatti', + 'Vladimir Lazic', + 'Vladimir Sharshov', + 'Wanderlust', + 'Wander Nauta', + 'Ward De Ridder', + 'WebCrusader', + 'webdr', + 'Wentao Tang', + 'wilana', + 'Wilfredo Ernesto Guerrero Campos', + 'Wim Champagne', + 'World Sucks', + 'Xabi Ezpeleta', + 'Xavi de Moner', + 'XavierToo', + 'XChesser', + 'Xiaodong Xu', + 'xyb', + 'Yaron', + 'Yasen Pramatarov', + 'YesPoX', + 'Yuren Ju', + 'Yves MATHIEU', + 'zekopeko', + 'zhuqin', + 'Zissan', + 'Γιάννης Κατσαμπίρης', + 'Артём Попов', + 'Миша', + 'Шаймарданов Максим', + '蔡查理', + ] + ) + ) + self.about.set_wrap_license(True) + self.about.set_license( + _( + 'This program is free software; you can 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. \n\n' + 'This program ' + '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. \n\n' + 'You should have received ' + 'a copy of the GNU General Public License along with this program; ' + 'if not, see <http://www.gnu.org/licenses>. \n\n' + '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. \n\n' + '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.' + ) + ) + self.about.set_website('http://deluge-torrent.org') + self.about.set_website_label('deluge-torrent.org') + + self.about.set_icon(get_deluge_icon()) + self.about.set_logo(get_pixbuf('deluge-about.png')) + + if client.connected(): + if not client.is_standalone(): + self.about.set_comments( + self.about.get_comments() + _('Server:') + ' %coreversion%\n' + ) + + self.about.set_comments( + self.about.get_comments() + '\n' + _('libtorrent:') + ' %ltversion%\n' + ) + + def on_lt_version(result): + c = self.about.get_comments() + c = c.replace('%ltversion%', result) + self.about.set_comments(c) + + def on_info(result): + c = self.about.get_comments() + c = c.replace('%coreversion%', result) + self.about.set_comments(c) + client.core.get_libtorrent_version().addCallback(on_lt_version) + + if not client.is_standalone(): + client.daemon.info().addCallback(on_info) + else: + client.core.get_libtorrent_version().addCallback(on_lt_version) + + def run(self): + self.about.show_all() + self.about.run() + self.about.destroy() diff --git a/deluge/ui/gtk3/addtorrentdialog.py b/deluge/ui/gtk3/addtorrentdialog.py new file mode 100644 index 0000000..aa71cc4 --- /dev/null +++ b/deluge/ui/gtk3/addtorrentdialog.py @@ -0,0 +1,1103 @@ +# +# Copyright (C) 2007 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. +# + +import logging +import os +from base64 import b64decode, b64encode +from xml.sax.saxutils import escape as xml_escape +from xml.sax.saxutils import unescape as xml_unescape + +from gi.repository import Gtk +from gi.repository.GObject import TYPE_INT64, TYPE_UINT64 + +import deluge.component as component +from deluge.bencode import bdecode +from deluge.common import ( + create_magnet_uri, + decode_bytes, + fsize, + get_magnet_info, + is_infohash, + is_magnet, + is_url, + resource_filename, +) +from deluge.configmanager import ConfigManager +from deluge.httpdownloader import download_file +from deluge.ui.client import client +from deluge.ui.common import TorrentInfo + +from .common import ( + get_clipboard_text, + listview_replace_treestore, + reparent_iter, + windowing, +) +from .dialogs import ErrorDialog +from .edittrackersdialog import trackers_tiers_from_text +from .path_chooser import PathChooser +from .torrentview_data_funcs import cell_data_size + +log = logging.getLogger(__name__) + + +class AddTorrentDialog(component.Component): + def __init__(self): + component.Component.__init__(self, 'AddTorrentDialog') + self.builder = Gtk.Builder() + # The base dialog + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'add_torrent_dialog.ui') + ) + ) + # The infohash dialog + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'add_torrent_dialog.infohash.ui') + ) + ) + # The url dialog + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'add_torrent_dialog.url.ui') + ) + ) + + self.dialog = self.builder.get_object('dialog_add_torrent') + + self.dialog.connect('delete-event', self._on_delete_event) + + self.builder.connect_signals(self) + + # download?, path, filesize, sequence number, inconsistent? + self.files_treestore = Gtk.TreeStore( + bool, str, TYPE_UINT64, TYPE_INT64, bool, str + ) + self.files_treestore.set_sort_column_id(1, Gtk.SortType.ASCENDING) + + # Holds the files info + self.files = {} + self.infos = {} + self.core_config = {} + self.options = {} + + self.previous_selected_torrent = None + + self.listview_torrents = self.builder.get_object('listview_torrents') + self.listview_files = self.builder.get_object('listview_files') + + self.prefetching_magnets = [] + + render = Gtk.CellRendererText() + render.connect('edited', self._on_torrent_name_edit) + render.set_property('editable', True) + column = Gtk.TreeViewColumn(_('Torrent'), render, text=1) + self.listview_torrents.append_column(column) + + render = Gtk.CellRendererToggle() + render.connect('toggled', self._on_file_toggled) + column = Gtk.TreeViewColumn(None, render, active=0, inconsistent=4) + self.listview_files.append_column(column) + + column = Gtk.TreeViewColumn(_('Filename')) + render = Gtk.CellRendererPixbuf() + column.pack_start(render, False) + column.add_attribute(render, 'icon-name', 5) + render = Gtk.CellRendererText() + render.set_property('editable', True) + render.connect('edited', self._on_filename_edited) + column.pack_start(render, True) + column.add_attribute(render, 'text', 1) + column.set_expand(True) + self.listview_files.append_column(column) + + render = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(_('Size')) + column.pack_start(render, True) + column.set_cell_data_func(render, cell_data_size, 2) + self.listview_files.append_column(column) + + self.torrent_liststore = Gtk.ListStore(str, str, str) + self.listview_torrents.set_model(self.torrent_liststore) + self.listview_torrents.set_tooltip_column(2) + self.listview_files.set_model(self.files_treestore) + + self.listview_files.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + self.listview_torrents.get_selection().connect( + 'changed', self._on_torrent_changed + ) + self.torrent_liststore.connect('row-inserted', self.update_dialog_title_count) + self.torrent_liststore.connect('row-deleted', self.update_dialog_title_count) + + self.setup_move_completed_path_chooser() + self.setup_download_location_path_chooser() + + # Get default config values from the core + self.core_keys = [ + 'pre_allocate_storage', + 'max_connections_per_torrent', + 'max_upload_slots_per_torrent', + 'max_upload_speed_per_torrent', + 'max_download_speed_per_torrent', + 'prioritize_first_last_pieces', + 'sequential_download', + 'add_paused', + 'download_location', + 'download_location_paths_list', + 'move_completed', + 'move_completed_path', + 'move_completed_paths_list', + 'super_seeding', + ] + # self.core_keys += self.move_completed_path_chooser.get_config_keys() + self.builder.get_object('notebook1').connect( + 'switch-page', self._on_switch_page + ) + + def start(self): + self.update_core_config() + + def show(self, focus=False): + self.update_core_config(True, focus) + + def _show(self, focus=False): + main_window = component.get('MainWindow') + if main_window.is_on_active_workspace(): + self.dialog.set_transient_for(main_window.window) + else: + self.dialog.set_transient_for(None) + self.dialog.set_position(Gtk.WindowPosition.CENTER) + + if focus: + timestamp = main_window.get_timestamp() + if windowing('X11'): + # Use present with X11 set_user_time since + # present_with_time is inconsistent. + self.dialog.present() + self.dialog.get_window().set_user_time(timestamp) + else: + self.dialog.present_with_time(timestamp) + else: + self.dialog.present() + + def hide(self): + self.dialog.hide() + self.files = {} + self.infos = {} + self.options = {} + self.previous_selected_torrent = None + self.torrent_liststore.clear() + self.files_treestore.clear() + self.prefetching_magnets = [] + self.dialog.set_transient_for(component.get('MainWindow').window) + + def _on_config_values(self, config, show=False, focus=False): + self.core_config = config + if self.core_config: + self.set_default_options() + if show: + self._show(focus) + + def update_core_config(self, show=False, focus=False): + # Send requests to the core for these config values + d = client.core.get_config_values(self.core_keys) + d.addCallback(self._on_config_values, show, focus) + + def _add_torrent_liststore(self, info_hash, name, filename, files, filedata): + """Add a torrent to torrent_liststore.""" + if info_hash in self.files: + return False + + torrent_row = [info_hash, name, xml_escape(filename)] + row_iter = self.torrent_liststore.append(torrent_row) + self.files[info_hash] = files + self.infos[info_hash] = filedata + self.listview_torrents.get_selection().select_iter(row_iter) + + self.set_default_options() + self.save_torrent_options(row_iter) + + return row_iter + + def update_dialog_title_count(self, *args): + """Update the AddTorrent dialog title with current torrent count.""" + self.dialog.set_title(_('Add Torrents (%d)') % len(self.torrent_liststore)) + + def show_already_added_dialog(self, count): + """Show a message about trying to add duplicate torrents.""" + log.debug('Tried to add %d duplicate torrents!', count) + ErrorDialog( + _('Duplicate torrent(s)'), + _( + 'You cannot add the same torrent twice.' + ' %d torrents were already added.' % count + ), + self.dialog, + ).run() + + def add_from_files(self, filenames): + already_added = 0 + + for filename in filenames: + # Get the torrent data from the torrent file + try: + info = TorrentInfo(filename) + except Exception as ex: + log.debug('Unable to open torrent file: %s', ex) + ErrorDialog(_('Invalid File'), ex, self.dialog).run() + continue + + if not self._add_torrent_liststore( + info.info_hash, info.name, filename, info.files, info.filedata + ): + already_added += 1 + + if already_added: + self.show_already_added_dialog(already_added) + + def _on_uri_metadata(self, result, uri, trackers): + """Process prefetched metadata to allow file priority selection.""" + info_hash, metadata = result + log.debug('magnet metadata for %s (%s)', uri, info_hash) + if info_hash not in self.prefetching_magnets: + return + + if metadata: + metadata = bdecode(b64decode(metadata)) + info = TorrentInfo.from_metadata(metadata, [[t] for t in trackers]) + self.files[info_hash] = info.files + self.infos[info_hash] = info.filedata + else: + log.info('Unable to fetch metadata for magnet: %s', uri) + self.prefetching_magnets.remove(info_hash) + self._on_torrent_changed(self.listview_torrents.get_selection()) + + def _on_uri_metadata_fail(self, result, info_hash): + self.prefetching_magnets.remove(info_hash) + self._on_torrent_changed(self.listview_torrents.get_selection()) + + def prefetch_waiting_message(self, torrent_id, files): + """Show magnet files fetching or failed message above files list.""" + if torrent_id in self.prefetching_magnets: + self.builder.get_object('prefetch_label').set_text( + _('Please wait for files...') + ) + self.builder.get_object('prefetch_spinner').show() + self.builder.get_object('prefetch_hbox').show() + elif not files: + self.builder.get_object('prefetch_label').set_text( + _('Unable to download files for this magnet') + ) + self.builder.get_object('prefetch_spinner').hide() + self.builder.get_object('prefetch_hbox').show() + else: + self.builder.get_object('prefetch_hbox').hide() + + def add_from_magnets(self, uris): + """Add a list of magnet URIs to torrent_liststore.""" + already_added = 0 + + for uri in uris: + magnet = get_magnet_info(uri) + if not magnet: + log.error('Invalid magnet: %s', uri) + continue + + torrent_id = magnet['info_hash'] + files = magnet['files_tree'] + if not self._add_torrent_liststore( + torrent_id, magnet['name'], uri, files, None + ): + already_added += 1 + continue + + if files: + continue + + self.prefetching_magnets.append(torrent_id) + self.prefetch_waiting_message(torrent_id, None) + d = client.core.prefetch_magnet_metadata(uri) + d.addCallback(self._on_uri_metadata, uri, magnet['trackers']) + d.addErrback(self._on_uri_metadata_fail, torrent_id) + + if already_added: + self.show_already_added_dialog(already_added) + + def _on_torrent_changed(self, treeselection): + (model, row) = treeselection.get_selected() + if row is None or not model.iter_is_valid(row): + self.files_treestore.clear() + self.previous_selected_torrent = None + return + + if model[row][0] not in self.files: + self.files_treestore.clear() + self.previous_selected_torrent = None + return + + # Save the previous torrents options + self.save_torrent_options() + + torrent_id = model.get_value(row, 0) + # Update files list + files_list = self.files[torrent_id] + self.prepare_file_store(files_list) + + if self.core_config == {}: + self.update_core_config() + + # Update the options frame + self.update_torrent_options(torrent_id) + # Update magnet prefetch message + self.prefetch_waiting_message(torrent_id, files_list) + + self.previous_selected_torrent = row + + def _on_torrent_name_edit(self, w, row, new_name): + # TODO: Update torrent name + pass + + def _on_switch_page(self, widget, page, page_num): + # Save the torrent options when switching notebook pages + self.save_torrent_options() + + def prepare_file_store(self, files): + with listview_replace_treestore(self.listview_files): + split_files = {} + for idx, _file in enumerate(files): + self.prepare_file( + _file, _file['path'], idx, _file.get('download', True), split_files + ) + self.add_files(None, split_files) + root = Gtk.TreePath.new_first() + self.listview_files.expand_row(root, False) + + def prepare_file(self, _file, file_name, file_num, download, files_storage): + first_slash_index = file_name.find('/') + if first_slash_index == -1: + files_storage[file_name] = (file_num, _file, download) + else: + file_name_chunk = file_name[: first_slash_index + 1] + if file_name_chunk not in files_storage: + files_storage[file_name_chunk] = {} + self.prepare_file( + _file, + file_name[first_slash_index + 1 :], + file_num, + download, + files_storage[file_name_chunk], + ) + + def add_files(self, parent_iter, split_files): + ret = 0 + for key, value in split_files.items(): + if key.endswith('/'): + chunk_iter = self.files_treestore.append( + parent_iter, [True, key, 0, -1, False, 'folder-symbolic'] + ) + chunk_size = self.add_files(chunk_iter, value) + self.files_treestore.set(chunk_iter, 2, chunk_size) + ret += chunk_size + else: + self.files_treestore.append( + parent_iter, + [ + value[2], + key, + value[1]['size'], + value[0], + False, + 'text-x-generic-symbolic', + ], + ) + ret += value[1]['size'] + if parent_iter and self.files_treestore.iter_has_child(parent_iter): + # Iterate through the children and see what we should label the + # folder, download true, download false or inconsistent. + itr = self.files_treestore.iter_children(parent_iter) + download = [] + download_value = False + inconsistent = False + while itr: + download.append(self.files_treestore.get_value(itr, 0)) + itr = self.files_treestore.iter_next(itr) + + if sum(download) == len(download): + download_value = True + elif sum(download) == 0: + download_value = False + else: + inconsistent = True + + self.files_treestore.set_value(parent_iter, 0, download_value) + self.files_treestore.set_value(parent_iter, 4, inconsistent) + return ret + + def load_path_choosers_data(self): + self.move_completed_path_chooser.set_text( + self.core_config['move_completed_path'], cursor_end=False, default_text=True + ) + self.download_location_path_chooser.set_text( + self.core_config['download_location'], cursor_end=False, default_text=True + ) + self.builder.get_object('chk_move_completed').set_active( + self.core_config['move_completed'] + ) + self.move_completed_path_chooser.set_sensitive( + self.core_config['move_completed'] + ) + + def setup_move_completed_path_chooser(self): + self.move_completed_hbox = self.builder.get_object( + 'hbox_move_completed_chooser' + ) + self.move_completed_path_chooser = PathChooser( + 'move_completed_paths_list', parent=self.dialog + ) + self.move_completed_hbox.add(self.move_completed_path_chooser) + self.move_completed_hbox.show_all() + + def setup_download_location_path_chooser(self): + self.download_location_hbox = self.builder.get_object( + 'hbox_download_location_chooser' + ) + self.download_location_path_chooser = PathChooser( + 'download_location_paths_list', parent=self.dialog + ) + self.download_location_hbox.add(self.download_location_path_chooser) + self.download_location_hbox.show_all() + + def update_torrent_options(self, torrent_id): + if torrent_id not in self.options: + self.set_default_options() + return + + options = self.options[torrent_id] + + self.download_location_path_chooser.set_text( + options['download_location'], cursor_end=True + ) + self.move_completed_path_chooser.set_text( + options['move_completed_path'], cursor_end=True + ) + + self.builder.get_object('spin_maxdown').set_value(options['max_download_speed']) + self.builder.get_object('spin_maxup').set_value(options['max_upload_speed']) + self.builder.get_object('spin_maxconnections').set_value( + options['max_connections'] + ) + self.builder.get_object('spin_maxupslots').set_value( + options['max_upload_slots'] + ) + self.builder.get_object('chk_paused').set_active(options['add_paused']) + self.builder.get_object('chk_pre_alloc').set_active( + options['pre_allocate_storage'] + ) + self.builder.get_object('chk_prioritize').set_active( + options['prioritize_first_last_pieces'] + ) + self.builder.get_object('chk_sequential_download').set_active( + options['sequential_download'] + ) + self.builder.get_object('chk_move_completed').set_active( + options['move_completed'] + ) + self.builder.get_object('chk_super_seeding').set_active( + options['super_seeding'] + ) + + def save_torrent_options(self, row=None): + # Keeps the torrent options dictionary up-to-date with what the user has + # selected. + if row is None: + if self.previous_selected_torrent and self.torrent_liststore.iter_is_valid( + self.previous_selected_torrent + ): + row = self.previous_selected_torrent + else: + return + + torrent_id = self.torrent_liststore.get_value(row, 0) + + if torrent_id in self.options: + options = self.options[torrent_id] + else: + options = {} + + options['download_location'] = decode_bytes( + self.download_location_path_chooser.get_text() + ) + options['move_completed_path'] = decode_bytes( + self.move_completed_path_chooser.get_text() + ) + options['pre_allocate_storage'] = self.builder.get_object( + 'chk_pre_alloc' + ).get_active() + options['move_completed'] = self.builder.get_object( + 'chk_move_completed' + ).get_active() + options['max_download_speed'] = self.builder.get_object( + 'spin_maxdown' + ).get_value() + options['max_upload_speed'] = self.builder.get_object('spin_maxup').get_value() + options['max_connections'] = self.builder.get_object( + 'spin_maxconnections' + ).get_value_as_int() + options['max_upload_slots'] = self.builder.get_object( + 'spin_maxupslots' + ).get_value_as_int() + options['add_paused'] = self.builder.get_object('chk_paused').get_active() + options['prioritize_first_last_pieces'] = self.builder.get_object( + 'chk_prioritize' + ).get_active() + options['sequential_download'] = ( + self.builder.get_object('chk_sequential_download').get_active() or False + ) + options['move_completed'] = self.builder.get_object( + 'chk_move_completed' + ).get_active() + options['seed_mode'] = self.builder.get_object('chk_seed_mode').get_active() + options['super_seeding'] = self.builder.get_object( + 'chk_super_seeding' + ).get_active() + + self.options[torrent_id] = options + + # Save the file priorities + files_priorities = self.build_priorities( + self.files_treestore.get_iter_first(), {} + ) + + if len(files_priorities) > 0: + for i, file_dict in enumerate(self.files[torrent_id]): + file_dict['download'] = files_priorities[i] + + def build_priorities(self, _iter, priorities): + while _iter is not None: + if self.files_treestore.iter_has_child(_iter): + self.build_priorities( + self.files_treestore.iter_children(_iter), priorities + ) + elif not self.files_treestore.get_value(_iter, 1).endswith('/'): + priorities[ + self.files_treestore.get_value(_iter, 3) + ] = self.files_treestore.get_value(_iter, 0) + _iter = self.files_treestore.iter_next(_iter) + return priorities + + def set_default_options(self): + if not self.core_config: + # update_core_config will call this method again. + self.update_core_config() + return + + self.load_path_choosers_data() + + self.builder.get_object('chk_pre_alloc').set_active( + self.core_config['pre_allocate_storage'] + ) + self.builder.get_object('spin_maxdown').set_value( + self.core_config['max_download_speed_per_torrent'] + ) + self.builder.get_object('spin_maxup').set_value( + self.core_config['max_upload_speed_per_torrent'] + ) + self.builder.get_object('spin_maxconnections').set_value( + self.core_config['max_connections_per_torrent'] + ) + self.builder.get_object('spin_maxupslots').set_value( + self.core_config['max_upload_slots_per_torrent'] + ) + self.builder.get_object('chk_paused').set_active(self.core_config['add_paused']) + self.builder.get_object('chk_prioritize').set_active( + self.core_config['prioritize_first_last_pieces'] + ) + self.builder.get_object('chk_sequential_download').set_active( + self.core_config['sequential_download'] + ) + self.builder.get_object('chk_move_completed').set_active( + self.core_config['move_completed'] + ) + self.builder.get_object('chk_seed_mode').set_active(False) + self.builder.get_object('chk_super_seeding').set_active( + self.core_config['super_seeding'] + ) + + def get_file_priorities(self, torrent_id): + # A list of priorities + files_list = [] + + for file_dict in self.files[torrent_id]: + if not file_dict['download']: + files_list.append(0) + else: + # Default lt file priority is 4 + files_list.append(4) + + return files_list + + def _on_file_toggled(self, render, path): + (model, paths) = self.listview_files.get_selection().get_selected_rows() + if len(paths) > 1: + for path in paths: + row = model.get_iter(path) + self.toggle_iter(row) + else: + row = model.get_iter(path) + self.toggle_iter(row) + self.update_treeview_toggles(self.files_treestore.get_iter_first()) + + def toggle_iter(self, _iter, toggle_to=None): + if toggle_to is None: + toggle_to = not self.files_treestore.get_value(_iter, 0) + self.files_treestore.set_value(_iter, 0, toggle_to) + if self.files_treestore.iter_has_child(_iter): + child = self.files_treestore.iter_children(_iter) + while child is not None: + self.toggle_iter(child, toggle_to) + child = self.files_treestore.iter_next(child) + + def update_treeview_toggles(self, _iter): + toggle_inconsistent = -1 + this_level_toggle = None + while _iter is not None: + if self.files_treestore.iter_has_child(_iter): + toggle = self.update_treeview_toggles( + self.files_treestore.iter_children(_iter) + ) + if toggle == toggle_inconsistent: + self.files_treestore.set_value(_iter, 4, True) + else: + self.files_treestore.set_value(_iter, 0, toggle) + # set inconsistent to false + self.files_treestore.set_value(_iter, 4, False) + else: + toggle = self.files_treestore.get_value(_iter, 0) + if this_level_toggle is None: + this_level_toggle = toggle + elif this_level_toggle != toggle: + this_level_toggle = toggle_inconsistent + _iter = self.files_treestore.iter_next(_iter) + return this_level_toggle + + def on_button_file_clicked(self, widget): + log.debug('on_button_file_clicked') + # Setup the filechooserdialog + chooser = Gtk.FileChooserDialog( + _('Choose a .torrent file'), + None, + Gtk.FileChooserAction.OPEN, + buttons=( + _('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_Open'), + Gtk.ResponseType.OK, + ), + ) + + chooser.set_transient_for(self.dialog) + chooser.set_select_multiple(True) + chooser.set_property('skip-taskbar-hint', True) + chooser.set_local_only(False) + + # Add .torrent and * file filters + file_filter = Gtk.FileFilter() + file_filter.set_name(_('Torrent files')) + file_filter.add_pattern('*.' + 'torrent') + chooser.add_filter(file_filter) + file_filter = Gtk.FileFilter() + file_filter.set_name(_('All files')) + file_filter.add_pattern('*') + chooser.add_filter(file_filter) + + # Load the 'default_load_path' from the config + self.config = ConfigManager('gtk3ui.conf') + if ( + 'default_load_path' in self.config + and self.config['default_load_path'] is not None + ): + chooser.set_current_folder(self.config['default_load_path']) + + # Run the dialog + response = chooser.run() + + if response == Gtk.ResponseType.OK: + result = [decode_bytes(f) for f in chooser.get_filenames()] + self.config['default_load_path'] = decode_bytes( + chooser.get_current_folder() + ) + else: + chooser.destroy() + return + + chooser.destroy() + self.add_from_files(result) + + def on_button_url_clicked(self, widget): + log.debug('on_button_url_clicked') + dialog = self.builder.get_object('url_dialog') + entry = self.builder.get_object('entry_url') + + dialog.set_default_response(Gtk.ResponseType.OK) + dialog.set_transient_for(self.dialog) + entry.grab_focus() + + text = get_clipboard_text() + if text and is_url(text) or is_magnet(text): + entry.set_text(text) + + dialog.show_all() + response = dialog.run() + + if response == Gtk.ResponseType.OK: + url = decode_bytes(entry.get_text()) + else: + url = None + + entry.set_text('') + dialog.hide() + + # This is where we need to fetch the .torrent file from the URL and + # add it to the list. + log.debug('url: %s', url) + if url: + if is_url(url): + self.add_from_url(url) + elif is_magnet(url): + self.add_from_magnets([url]) + else: + ErrorDialog( + _('Invalid URL'), + '{} {}'.format(url, _('is not a valid URL.')), + self.dialog, + ).run() + + def add_from_url(self, url): + dialog = Gtk.Dialog( + _('Downloading...'), + flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + parent=self.dialog, + ) + dialog.set_transient_for(self.dialog) + + pb = Gtk.ProgressBar() + dialog.vbox.pack_start(pb, True, True, 0) + dialog.show_all() + + # Create a tmp file path + import tempfile + + tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent') + + def on_part(data, current_length, total_length): + if total_length: + percent = current_length / total_length + pb.set_fraction(percent) + pb.set_text( + '%.2f%% (%s / %s)' + % (percent * 100, fsize(current_length), fsize(total_length)) + ) + else: + pb.pulse() + pb.set_text('%s' % fsize(current_length)) + + def on_download_success(result): + self.add_from_files([result]) + dialog.destroy() + + def on_download_fail(result): + log.debug('Download failed: %s', result) + dialog.destroy() + ErrorDialog( + _('Download Failed'), + '{} {}'.format(_('Failed to download:'), url), + details=result.getErrorMessage(), + parent=self.dialog, + ).run() + return result + + d = download_file(url, tmp_file, on_part) + os.close(tmp_fd) + d.addCallbacks(on_download_success, on_download_fail) + + def on_button_hash_clicked(self, widget): + log.debug('on_button_hash_clicked') + dialog = self.builder.get_object('dialog_infohash') + entry = self.builder.get_object('entry_hash') + textview = self.builder.get_object('text_trackers') + + dialog.set_default_response(Gtk.ResponseType.OK) + dialog.set_transient_for(self.dialog) + entry.grab_focus() + + text = get_clipboard_text() + if is_infohash(text): + entry.set_text(text) + + dialog.show_all() + response = dialog.run() + infohash = decode_bytes(entry.get_text()).strip() + if response == Gtk.ResponseType.OK and is_infohash(infohash): + # Create a list of trackers from the textview buffer + tview_buf = textview.get_buffer() + trackers_text = decode_bytes( + tview_buf.get_text(*tview_buf.get_bounds(), include_hidden_chars=False) + ) + log.debug('Create torrent tracker lines: %s', trackers_text) + trackers = list(trackers_tiers_from_text(trackers_text).keys()) + + # Convert the information to a magnet URI, this is just easier to + # handle this way. + log.debug('trackers: %s', trackers) + magnet = create_magnet_uri(infohash, infohash, trackers) + log.debug('magnet URI: %s', magnet) + self.add_from_magnets([magnet]) + + entry.set_text('') + textview.get_buffer().set_text('') + dialog.hide() + + def on_button_remove_clicked(self, widget): + log.debug('on_button_remove_clicked') + (model, row) = self.listview_torrents.get_selection().get_selected() + if row is None: + return + + torrent_id = model.get_value(row, 0) + + model.remove(row) + del self.files[torrent_id] + del self.infos[torrent_id] + + def on_button_trackers_clicked(self, widget): + log.debug('on_button_trackers_clicked') + + def on_button_cancel_clicked(self, widget): + log.debug('on_button_cancel_clicked') + self.hide() + + def on_button_add_clicked(self, widget): + log.debug('on_button_add_clicked') + self.add_torrents() + self.hide() + + def add_torrents(self): + (model, row) = self.listview_torrents.get_selection().get_selected() + if row is not None: + self.save_torrent_options(row) + + torrents_to_add = [] + + row = self.torrent_liststore.get_iter_first() + while row is not None: + torrent_id = self.torrent_liststore.get_value(row, 0) + filename = xml_unescape( + decode_bytes(self.torrent_liststore.get_value(row, 2)) + ) + try: + options = self.options[torrent_id] + except KeyError: + options = None + + file_priorities = self.get_file_priorities(torrent_id) + if options is not None: + options['file_priorities'] = file_priorities + + if self.infos[torrent_id]: + torrents_to_add.append( + ( + os.path.split(filename)[-1], + b64encode(self.infos[torrent_id]), + options, + ) + ) + elif is_magnet(filename): + client.core.add_torrent_magnet(filename, options).addErrback(log.debug) + + row = self.torrent_liststore.iter_next(row) + + def on_torrents_added(errors): + if errors: + log.info( + 'Failed to add %d out of %d torrents.', + len(errors), + len(torrents_to_add), + ) + for e in errors: + log.info('Torrent add failed: %s', e) + else: + log.info('Successfully added %d torrents.', len(torrents_to_add)) + + if torrents_to_add: + client.core.add_torrent_files(torrents_to_add).addCallback( + on_torrents_added + ) + + def on_button_apply_clicked(self, widget): + log.debug('on_button_apply_clicked') + (model, row) = self.listview_torrents.get_selection().get_selected() + if row is None: + return + + self.save_torrent_options(row) + + # The options, except file renames, we want all the torrents to have + options = self.options[model.get_value(row, 0)].copy() + options.pop('mapped_files', None) + + # Set all the torrent options + row = model.get_iter_first() + while row is not None: + torrent_id = model.get_value(row, 0) + self.options[torrent_id].update(options) + row = model.iter_next(row) + + def on_button_revert_clicked(self, widget): + log.debug('on_button_revert_clicked') + (model, row) = self.listview_torrents.get_selection().get_selected() + if row is None: + return + + del self.options[model.get_value(row, 0)] + self.set_default_options() + + def on_chk_move_completed_toggled(self, widget): + value = widget.get_active() + self.move_completed_path_chooser.set_sensitive(value) + + def _on_delete_event(self, widget, event): + self.hide() + return True + + def get_file_path(self, row, path=''): + if not row: + return path + + path = self.files_treestore[row][1] + path + return self.get_file_path(self.files_treestore.iter_parent(row), path) + + def _on_filename_edited(self, renderer, path, new_text): + index = self.files_treestore[path][3] + + # Ensure agnostic path separator + new_text = new_text.replace('\\', '/') + + new_text = new_text.strip('/').strip() + + # Return if the text hasn't changed + if new_text == self.files_treestore[path][1]: + return + + # Get the tree iter + itr = self.files_treestore.get_iter(path) + + # Get the torrent_id + (model, row) = self.listview_torrents.get_selection().get_selected() + torrent_id = model[row][0] + + if 'mapped_files' not in self.options[torrent_id]: + self.options[torrent_id]['mapped_files'] = {} + + if index > -1: + # We're renaming a file! Yay! That's easy! + if not new_text: + return + parent = self.files_treestore.iter_parent(itr) + file_path = os.path.join(self.get_file_path(parent), new_text) + # Don't rename if filename exists + if parent: + for row in self.files_treestore[parent].iterchildren(): + if new_text == row[1]: + return + if '/' in new_text: + # There are folders in this path, so we need to create them + # and then move the file iter to top + split_text = new_text.split('/') + for s in split_text[:-1]: + parent = self.files_treestore.append( + parent, [True, s, 0, -1, False, 'folder-symbolic'] + ) + + self.files_treestore[itr][1] = split_text[-1] + reparent_iter(self.files_treestore, itr, parent) + else: + # Update the row's text + self.files_treestore[itr][1] = new_text + + # Update the mapped_files dict in the options with the index and new + # file path. + # We'll send this to the core when adding the torrent so it knows + # what to rename before adding. + self.options[torrent_id]['mapped_files'][index] = file_path + self.files[torrent_id][index]['path'] = file_path + else: + # Folder! + def walk_tree(row): + if not row: + return + + # Get the file path base once, since it will be the same for + # all siblings + file_path_base = self.get_file_path( + self.files_treestore.iter_parent(row) + ) + + # Iterate through all the siblings at this level + while row: + # We recurse if there are children + if self.files_treestore.iter_has_child(row): + walk_tree(self.files_treestore.iter_children(row)) + + index = self.files_treestore[row][3] + + if index > -1: + # Get the new full path for this file + file_path = file_path_base + self.files_treestore[row][1] + + # Update the file path in the mapped_files dict + self.options[torrent_id]['mapped_files'][index] = file_path + self.files[torrent_id][index]['path'] = file_path + + # Get the next siblings iter + row = self.files_treestore.iter_next(row) + + # Update the treestore row first so that when walking the tree + # we can construct the new proper paths + + # We need to check if this folder has been split + if '/' in new_text: + # It's been split, so we need to add new folders and then re-parent + # itr. + parent = self.files_treestore.iter_parent(itr) + split_text = new_text.split('/') + for s in split_text[:-1]: + # We don't iterate over the last item because we'll just use + # the existing itr and change the text + parent = self.files_treestore.append( + parent, [True, s + '/', 0, -1, False, 'folder-symbolic'] + ) + + self.files_treestore[itr][1] = split_text[-1] + '/' + + # Now re-parent itr to parent + reparent_iter(self.files_treestore, itr, parent) + itr = parent + + # We need to re-expand the view because it might contracted + # if we change the root iter + root = Gtk.TreePath.new_first() + self.listview_files.expand_row(root, False) + else: + # This was a simple folder rename without any splits, so just + # change the path for itr + self.files_treestore[itr][1] = new_text + '/' + + # Walk through the tree from 'itr' and add all the new file paths + # to the 'mapped_files' option + walk_tree(itr) diff --git a/deluge/ui/gtk3/common.py b/deluge/ui/gtk3/common.py new file mode 100644 index 0000000..42a14b4 --- /dev/null +++ b/deluge/ui/gtk3/common.py @@ -0,0 +1,435 @@ +# +# Copyright (C) 2008 Marcos Mobley ('markybob') <markybob@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. +# +"""Common functions for various parts of gtkui to use.""" +import contextlib +import logging +import os +import pickle +import shutil +import sys + +from gi.repository.Gdk import SELECTION_CLIPBOARD, SELECTION_PRIMARY, Display +from gi.repository.GdkPixbuf import Colorspace, Pixbuf +from gi.repository.GLib import GError +from gi.repository.Gtk import ( + Clipboard, + IconTheme, + Menu, + MenuItem, + RadioMenuItem, + SeparatorMenuItem, + SortType, +) + +from deluge.common import get_pixmap, is_ip, osx_check, windows_check + +log = logging.getLogger(__name__) + + +def cmp(x, y): + """Replacement for built-in function cmp that was removed in Python 3. + + Compare the two objects x and y and return an integer according to + the outcome. The return value is negative if x < y, zero if x == y + and strictly positive if x > y. + """ + + try: + return (x > y) - (x < y) + except TypeError: + # Handle NoneType comparison + if x is None: + if y is None: + return 0 + return -1 + elif y is None: + return 1 + else: + raise + + +def create_blank_pixbuf(size=16): + pix = Pixbuf.new(Colorspace.RGB, True, 8, size, size) + pix.fill(0x0) + return pix + + +def get_pixbuf(filename: str, size: int = 0) -> Pixbuf: + """Creates a new pixbuf by loading an image from file + + Args: + filename: An image file to load + size: Specify a size constraint (equal aspect ratio) + + Returns: + A newly created pixbuf + + """ + # Skip ico and gif that cause Pixbuf crash on Windows + # https://dev.deluge-torrent.org/ticket/3501 + if windows_check() and filename.endswith(('.ico', '.gif')): + return create_blank_pixbuf(size) + + if not os.path.isabs(filename): + filename = get_pixmap(filename) + + pixbuf = None + try: + if size: + pixbuf = Pixbuf.new_from_file_at_size(filename, size, size) + else: + pixbuf = Pixbuf.new_from_file(filename) + except GError as ex: + # Failed to load the pixbuf (Bad image file), so return a blank pixbuf. + log.warning(ex) + + return pixbuf or create_blank_pixbuf(size or 16) + + +# Status icons.. Create them from file only once to avoid constantly re-creating them. +icon_downloading = get_pixbuf('downloading16.png') +icon_seeding = get_pixbuf('seeding16.png') +icon_inactive = get_pixbuf('inactive16.png') +icon_alert = get_pixbuf('alert16.png') +icon_queued = get_pixbuf('queued16.png') +icon_checking = get_pixbuf('checking16.png') + + +def get_logo(size): + """A Deluge logo. + + Params: + size (int): Size of logo in pixels + + Returns: + Pixbuf: deluge logo + """ + filename = 'deluge.svg' + if windows_check(): + filename = 'deluge.png' + return get_pixbuf(filename, size) + + +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: + Menu: The menu radio + """ + menu = Menu() + # Create menuitem to prevent unwanted toggled callback when creating menu. + menuitem = RadioMenuItem() + group = menuitem.get_group() + + 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): + item_text = str(value) + if suffix: + item_text += ' ' + suffix + menuitem = RadioMenuItem.new_with_label(group, item_text) + if pref_value and value == pref_value: + menuitem.set_active(True) + if callback: + menuitem.connect('toggled', callback) + menu.append(menuitem) + + if show_notset: + menuitem = RadioMenuItem.new_with_label(group, notset_label) + menuitem.set_name('unlimited') + if pref_value and pref_value < notset_lessthan: + menuitem.set_active(True) + menuitem.connect('toggled', callback) + menu.append(menuitem) + + if show_other: + menuitem = SeparatorMenuItem() + menu.append(menuitem) + menuitem = MenuItem.new_with_label(_('Other...')) + menuitem.set_name('other') + menuitem.connect('activate', callback) + menu.append(menuitem) + + return menu + + +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 + + 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_cols = treestore.append( + dest, treestore.get(i, *range(treestore.get_n_columns())) + ) + to_remove = i + if treestore.iter_children(i): + move_children(treestore.iter_children(i), n_cols) + if not move_siblings and i == src: + i = None + else: + i = treestore.iter_next(i) + + treestore.remove(to_remove) + + move_children(itr, parent) + + +def get_deluge_icon(): + """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: + Pixbuf: the deluge icon + """ + if windows_check(): + return get_logo(32) + else: + try: + icon_theme = IconTheme.get_default() + return icon_theme.load_icon('deluge', 64, 0) + except GError: + return get_logo(64) + + +def associate_magnet_links(overwrite=False): + """ + Associates magnet links to Deluge. + + Params: + overwrite (bool): if this is True, the current setting will be overwritten + + Returns: + bool: True if association was set + """ + + if windows_check(): + import winreg + + try: + hkey = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, 'Magnet') + except OSError: + overwrite = True + else: + winreg.CloseKey(hkey) + + if overwrite: + deluge_exe = os.path.join(os.path.dirname(sys.executable), 'deluge.exe') + try: + magnet_key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, 'Magnet') + except OSError: + # Could not create for all users, falling back to current user + magnet_key = winreg.CreateKey( + winreg.HKEY_CURRENT_USER, 'Software\\Classes\\Magnet' + ) + + winreg.SetValue(magnet_key, '', winreg.REG_SZ, 'URL:Magnet Protocol') + winreg.SetValueEx(magnet_key, 'URL Protocol', 0, winreg.REG_SZ, '') + winreg.SetValueEx(magnet_key, 'BrowserFlags', 0, winreg.REG_DWORD, 0x8) + winreg.SetValue(magnet_key, 'DefaultIcon', winreg.REG_SZ, f'{deluge_exe},0') + winreg.SetValue( + magnet_key, + r'shell\open\command', + winreg.REG_SZ, + f'"{deluge_exe}" "%1"', + ) + winreg.CloseKey(magnet_key) + + # Don't try associate magnet on OSX see: #2420 + elif not osx_check(): + # gconf method is only available in a GNOME environment + try: + import gi + + gi.require_version('GConf', '2.0') + from gi.repository import GConf + except ValueError: + log.debug( + 'gconf not available, so will not attempt to register magnet URI handler' + ) + return False + else: + key = '/desktop/gnome/url-handlers/magnet/command' + gconf_client = GConf.Client.get_default() + if (gconf_client.get(key) and overwrite) or not gconf_client.get(key): + # We are either going to overwrite the key, or do it if it hasn't been set yet + if gconf_client.set_string(key, 'deluge "%s"'): + gconf_client.set_bool( + '/desktop/gnome/url-handlers/magnet/needs_terminal', False + ) + gconf_client.set_bool( + '/desktop/gnome/url-handlers/magnet/enabled', True + ) + log.info('Deluge registered as default magnet URI handler!') + return True + else: + log.error( + 'Unable to register Deluge as default magnet URI handler.' + ) + return False + return False + + +def save_pickled_state_file(filename, state): + """Save a file in the config directory and creates a backup + + 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(), 'gtk3ui_state', filename) + filepath_bak = filepath + '.bak' + filepath_tmp = filepath + '.tmp' + + try: + if os.path.isfile(filepath): + log.debug('Creating backup of %s at: %s', filename, filepath_bak) + shutil.copy2(filepath, filepath_bak) + except OSError as ex: + log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex) + else: + log.info('Saving the %s at: %s', filename, filepath) + try: + with open(filepath_tmp, 'wb') as _file: + # Pickle the state object + pickle.dump(state, _file, protocol=2) + _file.flush() + os.fsync(_file.fileno()) + shutil.move(filepath_tmp, filepath) + except (OSError, EOFError, pickle.PicklingError) as ex: + log.error('Unable to save %s: %s', filename, ex) + if os.path.isfile(filepath_bak): + 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. + + 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(), 'gtk3ui_state', filename) + filepath_bak = filepath + '.bak' + + for _filepath in (filepath, filepath_bak): + log.info('Opening %s for load: %s', filename, _filepath) + try: + with open(_filepath, 'rb') as _file: + state = pickle.load(_file, encoding='utf8') + except (OSError, pickle.UnpicklingError) as ex: + log.warning('Unable to load %s: %s', _filepath, ex) + else: + log.info('Successfully loaded %s: %s', filename, _filepath) + return state + + +@contextlib.contextmanager +def listview_replace_treestore(listview): + """Prepare a listview's treestore to be entirely replaced. + + Params: + listview: a listview backed by a treestore + """ + # From http://faq.pygtk.org/index.py?req=show&file=faq13.043.htp + # "tips for improving performance when adding many rows to a Treeview" + listview.freeze_child_notify() + treestore = listview.get_model() + listview.set_model(None) + treestore.clear() + treestore.set_default_sort_func(lambda *args: 0) + original_sort = treestore.get_sort_column_id() + treestore.set_sort_column_id(-1, SortType.ASCENDING) + + yield + + if original_sort != (None, None): + treestore.set_sort_column_id(*original_sort) + + listview.set_model(treestore) + listview.thaw_child_notify() + + +def get_clipboard_text(): + text = ( + Clipboard.get(SELECTION_CLIPBOARD).wait_for_text() + or Clipboard.get(SELECTION_PRIMARY).wait_for_text() + ) + if text: + return text.strip() + + +def windowing(like): + return like.lower() in str(type(Display.get_default())).lower() + + +def parse_ip_port(text): + """Return an IP and port from text. + + Parses both IPv4 and IPv6. + + Params: + text (str): Text to be parsed for IP and port. + + Returns: + tuple: (ip (str), port (int)) + + """ + if '.' in text: + # ipv4 + ip, __, port = text.rpartition(':') + elif '[' in text: + # ipv6 + ip, __, port = text.partition('[')[2].partition(']:') + else: + return None, None + + if ip and is_ip(ip) and port.isdigit(): + return ip, int(port) + else: + return None, None diff --git a/deluge/ui/gtk3/connectionmanager.py b/deluge/ui/gtk3/connectionmanager.py new file mode 100644 index 0000000..b53dd8e --- /dev/null +++ b/deluge/ui/gtk3/connectionmanager.py @@ -0,0 +1,560 @@ +# +# Copyright (C) 2007-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. +# + +import logging +import os +from socket import gaierror, getaddrinfo +from urllib.parse import urlparse + +from gi.repository import Gtk +from twisted.internet import defer, reactor + +import deluge.component as component +from deluge.common import resource_filename, windows_check +from deluge.configmanager import ConfigManager, get_config_dir +from deluge.error import AuthenticationRequired, BadLoginError, IncompatibleClient +from deluge.ui.client import Client, client +from deluge.ui.hostlist import DEFAULT_PORT, LOCALHOST, HostList + +from .common import get_clipboard_text +from .dialogs import AuthenticationDialog, ErrorDialog + +log = logging.getLogger(__name__) + +HOSTLIST_COL_ID = 0 +HOSTLIST_COL_HOST = 1 +HOSTLIST_COL_PORT = 2 +HOSTLIST_COL_USER = 3 +HOSTLIST_COL_PASS = 4 +HOSTLIST_COL_STATUS = 5 +HOSTLIST_COL_VERSION = 6 +HOSTLIST_COL_STATUS_I18N = 7 + +HOSTLIST_ICONS = { + 'offline': 'action-unavailable-symbolic', + 'online': 'network-server-symbolic', + 'connected': 'network-transmit-receive-symbolic', +} +STATUS_I18N = { + 'offline': _('Offline'), + 'online': _('Online'), + 'connected': _('Connected'), +} + + +def cell_render_host(column, cell, model, row, data): + host, port, username = model.get(row, *data) + text = host + ':' + str(port) + if username: + text = username + '@' + text + cell.set_property('text', text) + + +def cell_render_status_icon(column, cell, model, row, data): + status = model[row][data] + status = status if status else 'offline' + icon_name = HOSTLIST_ICONS.get(status, None) + cell.set_property('icon-name', icon_name) + + +class ConnectionManager(component.Component): + def __init__(self): + component.Component.__init__(self, 'ConnectionManager') + self.gtkui_config = ConfigManager('gtk3ui.conf') + self.hostlist = HostList() + self.running = False + + # Component overrides + def start(self): + pass + + def stop(self): + # Close this dialog when we are shutting down + if self.running: + self.connection_manager.response(Gtk.ResponseType.CLOSE) + + def shutdown(self): + pass + + # Public methods + def show(self): + """Show the ConnectionManager dialog.""" + self.builder = Gtk.Builder() + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'connection_manager.ui') + ) + ) + self.connection_manager = self.builder.get_object('connection_manager') + self.connection_manager.set_transient_for(component.get('MainWindow').window) + + # Setup the hostlist liststore and treeview + self.treeview = self.builder.get_object('treeview_hostlist') + self.treeview.set_tooltip_column(HOSTLIST_COL_STATUS_I18N) + self.liststore = self.builder.get_object('liststore_hostlist') + + render = Gtk.CellRendererPixbuf() + column = Gtk.TreeViewColumn(_('Status'), render) + column.set_cell_data_func(render, cell_render_status_icon, HOSTLIST_COL_STATUS) + self.treeview.append_column(column) + + render = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(_('Host'), render, text=HOSTLIST_COL_HOST) + host_data = (HOSTLIST_COL_HOST, HOSTLIST_COL_PORT, HOSTLIST_COL_USER) + column.set_cell_data_func(render, cell_render_host, host_data) + column.set_expand(True) + self.treeview.append_column(column) + + column = Gtk.TreeViewColumn( + _('Version'), Gtk.CellRendererText(), text=HOSTLIST_COL_VERSION + ) + self.treeview.append_column(column) + + # Load any saved host entries + self._load_liststore() + # Set widgets to values from gtkui config. + self._load_widget_config() + self._update_widget_buttons() + + # Connect the signals to the handlers + self.builder.connect_signals(self) + self.treeview.get_selection().connect( + 'changed', self.on_hostlist_selection_changed + ) + + # Set running True before update status call. + self.running = True + + if windows_check(): + # Call to simulate() required to workaround showing daemon status (see #2813) + reactor.simulate() + self._update_host_status() + + # Trigger the on_selection_changed code and select the first host if possible + self.treeview.get_selection().unselect_all() + if len(self.liststore): + self.treeview.get_selection().select_path(0) + + # Run the dialog + self.connection_manager.run() + + # Dialog closed so cleanup. + self.running = False + self.connection_manager.destroy() + del self.builder + del self.connection_manager + del self.liststore + del self.treeview + + def _load_liststore(self): + """Load saved host entries""" + for host_entry in self.hostlist.get_hosts_info(): + host_id, host, port, username = host_entry + self.liststore.append([host_id, host, port, username, '', '', '', '']) + + def _load_widget_config(self): + """Set the widgets to show the correct options from the config.""" + self.builder.get_object('chk_autoconnect').set_active( + self.gtkui_config['autoconnect'] + ) + self.builder.get_object('chk_autostart').set_active( + self.gtkui_config['autostart_localhost'] + ) + self.builder.get_object('chk_donotshow').set_active( + not self.gtkui_config['show_connection_manager_on_start'] + ) + + def _update_host_status(self): + """Updates the host status""" + if not self.running: + # Callback likely fired after the window closed. + return + + def on_host_status(status_info, row): + if self.running and row: + status = status_info[1].lower() + row[HOSTLIST_COL_STATUS] = status + row[HOSTLIST_COL_STATUS_I18N] = STATUS_I18N[status] + row[HOSTLIST_COL_VERSION] = status_info[2] + self._update_widget_buttons() + + deferreds = [] + for row in self.liststore: + host_id = row[HOSTLIST_COL_ID] + d = self.hostlist.get_host_status(host_id) + try: + d.addCallback(on_host_status, row) + except AttributeError: + on_host_status(d, row) + else: + deferreds.append(d) + defer.DeferredList(deferreds) + + def _update_widget_buttons(self): + """Updates the dialog button states.""" + self.builder.get_object('button_refresh').set_sensitive(len(self.liststore)) + self.builder.get_object('button_startdaemon').set_sensitive(False) + self.builder.get_object('button_connect').set_sensitive(False) + self.builder.get_object('button_connect').set_label(_('C_onnect')) + self.builder.get_object('button_edithost').set_sensitive(False) + self.builder.get_object('button_removehost').set_sensitive(False) + self.builder.get_object('button_startdaemon').set_sensitive(False) + self.builder.get_object('image_startdaemon').set_from_icon_name( + 'system-run-symbolic', Gtk.IconSize.BUTTON + ) + self.builder.get_object('label_startdaemon').set_text_with_mnemonic( + _('_Start Daemon') + ) + + model, row = self.treeview.get_selection().get_selected() + if row: + self.builder.get_object('button_edithost').set_sensitive(True) + self.builder.get_object('button_removehost').set_sensitive(True) + else: + return + + # Get selected host info. + __, host, port, __, __, status, __, __ = model[row] + + try: + getaddrinfo(host, None) + except gaierror as ex: + log.error( + 'Error resolving host %s to ip: %s', row[HOSTLIST_COL_HOST], ex.args[1] + ) + self.builder.get_object('button_connect').set_sensitive(False) + return + + log.debug('Host Status: %s, %s', host, status) + + # Check to see if the host is online + if status == 'connected' or status == 'online': + self.builder.get_object('button_connect').set_sensitive(True) + self.builder.get_object('image_startdaemon').set_from_icon_name( + 'process-stop-symbolic', Gtk.IconSize.MENU + ) + self.builder.get_object('label_startdaemon').set_text_with_mnemonic( + _('_Stop Daemon') + ) + self.builder.get_object('button_startdaemon').set_sensitive(False) + if status == 'connected': + # Display a disconnect button if we're connected to this host + self.builder.get_object('button_connect').set_label(_('_Disconnect')) + self.builder.get_object('button_removehost').set_sensitive(False) + # Currently can only stop daemon when connected to it + self.builder.get_object('button_startdaemon').set_sensitive(True) + elif host in LOCALHOST: + # If localhost we can start the dameon. + self.builder.get_object('button_startdaemon').set_sensitive(True) + + def start_daemon(self, port, config): + """Attempts to start local daemon process and will show an ErrorDialog if not. + + Args: + port (int): Port for the daemon to listen on. + config (str): Config path to pass to daemon. + + Returns: + bool: True is successfully started the daemon, False otherwise. + + """ + if client.start_daemon(port, config): + log.debug('Localhost daemon started') + reactor.callLater(1, self._update_host_status) + return True + else: + ErrorDialog( + _('Unable to start daemon!'), + _('Check deluged package is installed and logs for further details'), + ).run() + return False + + # Signal handlers + def _connect(self, host_id, username=None, password=None, try_counter=0): + def do_connect(result, username=None, password=None, *args): + log.debug('Attempting to connect to daemon...') + for host_entry in self.hostlist.config['hosts']: + if host_entry[0] == host_id: + __, host, port, host_user, host_pass = host_entry + + username = username if username else host_user + password = password if password else host_pass + + d = client.connect(host, port, username, password) + d.addCallback(self._on_connect, host_id) + d.addErrback(self._on_connect_fail, host_id, try_counter) + return d + + if client.connected(): + return client.disconnect().addCallback(do_connect, username, password) + else: + return do_connect(None, username, password) + + def _on_connect(self, daemon_info, host_id): + log.debug('Connected to daemon: %s', host_id) + if self.gtkui_config['autoconnect']: + self.gtkui_config['autoconnect_host_id'] = host_id + if self.running: + # When connected to a client, and then trying to connect to another, + # this component will be stopped(while the connect deferred is + # running), so, self.connection_manager will be deleted. + # If that's not the case, close the dialog. + self.connection_manager.response(Gtk.ResponseType.OK) + component.start() + + def _on_connect_fail(self, reason, host_id, try_counter): + log.debug('Failed to connect: %s', reason.value) + + if reason.check(AuthenticationRequired, BadLoginError): + log.debug('PasswordRequired exception') + dialog = AuthenticationDialog(reason.value.message, reason.value.username) + + def dialog_finished(response_id): + if response_id == Gtk.ResponseType.OK: + self._connect(host_id, dialog.get_username(), dialog.get_password()) + + return dialog.run().addCallback(dialog_finished) + + elif reason.check(IncompatibleClient): + return ErrorDialog(_('Incompatible Client'), reason.value.message).run() + + if try_counter: + log.info('Retrying connection.. Retries left: %s', try_counter) + return reactor.callLater( + 0.5, self._connect, host_id, try_counter=try_counter - 1 + ) + + msg = str(reason.value) + if not self.gtkui_config['autostart_localhost']: + msg += '\n' + _( + 'Auto-starting the daemon locally is not enabled. ' + 'See "Options" on the "Connection Manager".' + ) + ErrorDialog(_('Failed To Connect'), msg).run() + + def on_button_connect_clicked(self, widget=None): + """Button handler for connect to or disconnect from daemon.""" + model, row = self.treeview.get_selection().get_selected() + if not row: + return + + host_id, host, port, __, __, status, __, __ = model[row] + # If status is connected then connect button disconnects instead. + if status == 'connected': + + def on_disconnect(reason): + self._update_host_status() + + return client.disconnect().addCallback(on_disconnect) + + try_counter = 0 + auto_start = self.builder.get_object('chk_autostart').get_active() + if auto_start and host in LOCALHOST and status == 'offline': + # Start the local daemon and then connect with retries set. + if self.start_daemon(port, get_config_dir()): + try_counter = 6 + else: + # Don't attempt to connect to offline daemon. + return + + self._connect(host_id, try_counter=try_counter) + + def on_button_close_clicked(self, widget): + self.connection_manager.response(Gtk.ResponseType.CLOSE) + + def _run_addhost_dialog(self, edit_host_info=None): + """Create and runs the add host dialog. + + Supplying edit_host_info changes the dialog to an edit dialog. + + Args: + edit_host_info (list): A list of (host, port, user, pass) to edit. + + Returns: + list: The new host info values (host, port, user, pass). + + """ + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'connection_manager.addhost.ui') + ) + ) + dialog = self.builder.get_object('addhost_dialog') + dialog.set_transient_for(self.connection_manager) + hostname_entry = self.builder.get_object('entry_hostname') + port_spinbutton = self.builder.get_object('spinbutton_port') + username_entry = self.builder.get_object('entry_username') + password_entry = self.builder.get_object('entry_password') + + if edit_host_info: + dialog.set_title(_('Edit Host')) + hostname_entry.set_text(edit_host_info[0]) + port_spinbutton.set_value(edit_host_info[1]) + username_entry.set_text(edit_host_info[2]) + password_entry.set_text(edit_host_info[3]) + + response = dialog.run() + new_host_info = [] + if response: + new_host_info.append(hostname_entry.get_text()) + new_host_info.append(port_spinbutton.get_value_as_int()) + new_host_info.append(username_entry.get_text()) + new_host_info.append(password_entry.get_text()) + + dialog.destroy() + return new_host_info + + def on_button_addhost_clicked(self, widget): + log.debug('on_button_addhost_clicked') + host_info = self._run_addhost_dialog() + if host_info: + hostname, port, username, password = host_info + try: + host_id = self.hostlist.add_host(hostname, port, username, password) + except ValueError as ex: + ErrorDialog(_('Error Adding Host'), ex).run() + else: + status = 'offline' + version = '' + self.liststore.append( + [ + host_id, + hostname, + port, + username, + password, + status, + version, + STATUS_I18N[status], + ] + ) + self._update_host_status() + + def on_button_edithost_clicked(self, widget=None): + log.debug('on_button_edithost_clicked') + model, row = self.treeview.get_selection().get_selected() + status = model[row][HOSTLIST_COL_STATUS] + host_id = model[row][HOSTLIST_COL_ID] + host_info = [ + self.liststore[row][HOSTLIST_COL_HOST], + self.liststore[row][HOSTLIST_COL_PORT], + self.liststore[row][HOSTLIST_COL_USER], + self.liststore[row][HOSTLIST_COL_PASS], + ] + + new_host_info = self._run_addhost_dialog(edit_host_info=host_info) + if new_host_info: + hostname, port, username, password = new_host_info + try: + self.hostlist.update_host(host_id, hostname, port, username, password) + except ValueError as ex: + ErrorDialog(_('Error Updating Host'), ex).run() + else: + self.liststore[row] = ( + host_id, + hostname, + port, + username, + password, + '', + '', + '', + ) + self._update_host_status() + + if status == 'connected': + + def on_disconnect(reason): + self._update_host_status() + + client.disconnect().addCallback(on_disconnect) + + def on_button_removehost_clicked(self, widget): + log.debug('on_button_removehost_clicked') + # Get the selected rows + model, row = self.treeview.get_selection().get_selected() + self.hostlist.remove_host(model[row][HOSTLIST_COL_ID]) + self.liststore.remove(row) + # Update the hostlist + self._update_host_status() + + def on_button_startdaemon_clicked(self, widget): + log.debug('on_button_startdaemon_clicked') + if not self.liststore.iter_n_children(None): + # There is nothing in the list, so lets create a localhost entry + try: + self.hostlist.add_default_host() + except ValueError as ex: + log.error('Error adding default host: %s', ex) + else: + self.start_daemon(DEFAULT_PORT, get_config_dir()) + finally: + return + + paths = self.treeview.get_selection().get_selected_rows()[1] + if len(paths): + __, host, port, user, password, status, __, __ = self.liststore[paths[0]] + else: + return + + if host not in LOCALHOST: + return + + def on_daemon_status_change(result): + """Daemon start/stop callback""" + reactor.callLater(0.7, self._update_host_status) + + if status in ('online', 'connected'): + # Button will stop the daemon if status is online or connected. + def on_connect(d, c): + """Client callback to call daemon shutdown""" + c.daemon.shutdown().addCallback(on_daemon_status_change) + + if client.connected() and (host, port, user) == client.connection_info(): + client.daemon.shutdown().addCallback(on_daemon_status_change) + elif user and password: + c = Client() + c.connect(host, port, user, password).addCallback(on_connect, c) + else: + # Otherwise button will start the daemon. + self.start_daemon(port, get_config_dir()) + + def on_button_refresh_clicked(self, widget): + self._update_host_status() + + def on_hostlist_row_activated(self, tree, path, view_column): + self.on_button_connect_clicked() + + def on_hostlist_selection_changed(self, treeselection): + self._update_widget_buttons() + + def on_chk_toggled(self, widget): + self.gtkui_config['autoconnect'] = self.builder.get_object( + 'chk_autoconnect' + ).get_active() + self.gtkui_config['autostart_localhost'] = self.builder.get_object( + 'chk_autostart' + ).get_active() + self.gtkui_config[ + 'show_connection_manager_on_start' + ] = not self.builder.get_object('chk_donotshow').get_active() + + def on_entry_host_paste_clipboard(self, widget): + text = get_clipboard_text() + log.debug('on_entry_proxy_host_paste-clipboard: got paste: %s', text) + text = text if '//' in text else '//' + text + parsed = urlparse(text) + if parsed.hostname: + widget.set_text(parsed.hostname) + widget.emit_stop_by_name('paste-clipboard') + if parsed.port: + self.builder.get_object('spinbutton_port').set_value(parsed.port) + if parsed.username: + self.builder.get_object('entry_username').set_text(parsed.username) + if parsed.password: + self.builder.get_object('entry_password').set_text(parsed.password) diff --git a/deluge/ui/gtk3/createtorrentdialog.py b/deluge/ui/gtk3/createtorrentdialog.py new file mode 100644 index 0000000..e9f1690 --- /dev/null +++ b/deluge/ui/gtk3/createtorrentdialog.py @@ -0,0 +1,517 @@ +# +# Copyright (C) 2008 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. +# + +import logging +import os.path +from base64 import b64encode + +from gi.repository import Gtk +from gi.repository.GObject import TYPE_UINT64, idle_add +from twisted.internet.threads import deferToThread + +import deluge.component as component +from deluge.common import decode_bytes, get_path_size, is_url, resource_filename +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +from .edittrackersdialog import ( + last_tier_trackers_from_liststore, + trackers_tiers_from_text, +) +from .torrentview_data_funcs import cell_data_size + +log = logging.getLogger(__name__) + + +class CreateTorrentDialog: + def __init__(self): + pass + + def show(self): + self.builder = Gtk.Builder() + + ui_filenames = [ + 'create_torrent_dialog.ui', + 'create_torrent_dialog.remote_path.ui', + 'create_torrent_dialog.remote_save.ui', + 'create_torrent_dialog.progress.ui', + ] + for filename in ui_filenames: + self.builder.add_from_file( + resource_filename(__package__, os.path.join('glade', filename)) + ) + + self.config = ConfigManager('gtk3ui.conf') + + self.dialog = self.builder.get_object('create_torrent_dialog') + self.dialog.set_transient_for(component.get('MainWindow').window) + + self.builder.connect_signals(self) + + # path, icon, size + self.files_treestore = Gtk.TreeStore(str, str, TYPE_UINT64) + + column = Gtk.TreeViewColumn(_('Filename')) + render = Gtk.CellRendererPixbuf() + column.pack_start(render, False) + column.add_attribute(render, 'icon-name', 1) + render = Gtk.CellRendererText() + column.pack_start(render, True) + column.add_attribute(render, 'text', 0) + column.set_expand(True) + self.builder.get_object('treeview_files').append_column(column) + + column = Gtk.TreeViewColumn(_('Size')) + render = Gtk.CellRendererText() + column.pack_start(render, True) + column.set_cell_data_func(render, cell_data_size, 2) + self.builder.get_object('treeview_files').append_column(column) + + self.builder.get_object('treeview_files').set_model(self.files_treestore) + self.builder.get_object('treeview_files').set_show_expanders(False) + + # tier, url + self.trackers_liststore = Gtk.ListStore(int, str) + + self.builder.get_object('tracker_treeview').append_column( + Gtk.TreeViewColumn(_('Tier'), Gtk.CellRendererText(), text=0) + ) + self.builder.get_object('tracker_treeview').append_column( + Gtk.TreeViewColumn(_('Tracker'), Gtk.CellRendererText(), text=1) + ) + + self.builder.get_object('tracker_treeview').set_model(self.trackers_liststore) + self.trackers_liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) + + if not client.is_localhost() and client.connected(): + self.builder.get_object('button_remote_path').show() + else: + self.builder.get_object('button_remote_path').hide() + + self.dialog.show() + + def parse_piece_size_text(self, value): + psize, metric = value.split() + psize = int(psize) + if psize < 32: + # This is a MiB value + psize = psize * 1024 * 1024 + else: + # This is a KiB value + psize = psize * 1024 + + return psize + + def adjust_piece_size(self): + """Adjusts the recommended piece based on the file/folder/path selected.""" + size = self.files_treestore[0][2] + model = self.builder.get_object('combo_piece_size').get_model() + for index, value in enumerate(model): + psize = self.parse_piece_size_text(value[0]) + pieces = size // psize + if pieces < 2048 or (index + 1) == len(model): + self.builder.get_object('combo_piece_size').set_active(index) + break + + def on_button_file_clicked(self, widget): + log.debug('on_button_file_clicked') + # Setup the filechooserdialog + chooser = Gtk.FileChooserDialog( + _('Choose a file'), + self.dialog, + Gtk.FileChooserAction.OPEN, + buttons=( + _('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_Open'), + Gtk.ResponseType.OK, + ), + ) + + chooser.set_transient_for(self.dialog) + chooser.set_select_multiple(False) + chooser.set_property('skip-taskbar-hint', True) + + # Run the dialog + response = chooser.run() + + if response == Gtk.ResponseType.OK: + result = chooser.get_filename() + else: + chooser.destroy() + return + + path = decode_bytes(result) + + self.files_treestore.clear() + self.files_treestore.append( + None, [result, 'text-x-generic-symbolic', get_path_size(path)] + ) + self.adjust_piece_size() + chooser.destroy() + + def on_button_folder_clicked(self, widget): + log.debug('on_button_folder_clicked') + # Setup the filechooserdialog + chooser = Gtk.FileChooserDialog( + _('Choose a folder'), + self.dialog, + Gtk.FileChooserAction.SELECT_FOLDER, + buttons=( + _('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_Open'), + Gtk.ResponseType.OK, + ), + ) + + chooser.set_transient_for(self.dialog) + chooser.set_select_multiple(False) + chooser.set_property('skip-taskbar-hint', True) + # Run the dialog + response = chooser.run() + + if response == Gtk.ResponseType.OK: + result = chooser.get_filename() + else: + chooser.destroy() + return + + path = decode_bytes(result) + + self.files_treestore.clear() + self.files_treestore.append( + None, [result, 'document-open-symbolic', get_path_size(path)] + ) + self.adjust_piece_size() + chooser.destroy() + + def on_button_remote_path_clicked(self, widget): + log.debug('on_button_remote_path_clicked') + dialog = self.builder.get_object('remote_path_dialog') + entry = self.builder.get_object('entry_path') + dialog.set_transient_for(self.dialog) + entry.set_text('/') + entry.grab_focus() + response = dialog.run() + + if response == Gtk.ResponseType.OK: + result = entry.get_text() + + def _on_get_path_size(size): + log.debug('size: %s', size) + if size > 0: + self.files_treestore.clear() + self.files_treestore.append( + None, [result, 'network-workgroup-symbolic', size] + ) + self.adjust_piece_size() + + client.core.get_path_size(result).addCallback(_on_get_path_size) + client.force_call(True) + + dialog.hide() + + def on_button_cancel_clicked(self, widget): + log.debug('on_button_cancel_clicked') + self.dialog.destroy() + + def on_button_save_clicked(self, widget): + log.debug('on_button_save_clicked') + if len(self.files_treestore) == 0: + return + + # Get the path + path = self.files_treestore[0][0].rstrip('\\/') + torrent_filename = '%s.torrent' % os.path.split(path)[-1] + + is_remote = 'network' in self.files_treestore[0][1] + + if is_remote: + # This is a remote path + dialog = self.builder.get_object('remote_save_dialog') + dialog.set_transient_for(self.dialog) + dialog_save_path = self.builder.get_object('entry_save_path') + dialog_save_path.set_text(path + '.torrent') + response = dialog.run() + if response == Gtk.ResponseType.OK: + result = dialog_save_path.get_text() + else: + dialog.hide() + return + dialog.hide() + else: + # Setup the filechooserdialog + chooser = Gtk.FileChooserDialog( + _('Save .torrent file'), + self.dialog, + Gtk.FileChooserAction.SAVE, + buttons=( + _('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_Save'), + Gtk.ResponseType.OK, + ), + ) + + chooser.set_transient_for(self.dialog) + chooser.set_select_multiple(False) + chooser.set_property('skip-taskbar-hint', True) + + # Add .torrent and * file filters + file_filter = Gtk.FileFilter() + file_filter.set_name(_('Torrent files')) + file_filter.add_pattern('*.' + 'torrent') + chooser.add_filter(file_filter) + file_filter = Gtk.FileFilter() + file_filter.set_name(_('All files')) + file_filter.add_pattern('*') + chooser.add_filter(file_filter) + + chooser.set_current_name(torrent_filename) + # Run the dialog + response = chooser.run() + + if response == Gtk.ResponseType.OK: + result = chooser.get_filename() + else: + chooser.destroy() + return + chooser.destroy() + + # Fix up torrent filename + if len(result) < 9: + result += '.torrent' + elif result[-8:] != '.torrent': + result += '.torrent' + + # Get a list of trackers + trackers = [] + if not len(self.trackers_liststore): + tracker = None + else: + # Create a list of lists [[tier0, ...], [tier1, ...], ...] + tier_dict = {} + for tier, tracker in self.trackers_liststore: + tier_dict.setdefault(tier, []).append(tracker) + + trackers = [tier_dict[tier] for tier in sorted(tier_dict)] + # Get the first tracker in the first tier + tracker = trackers[0][0] + + # Get a list of webseeds + textview_buf = self.builder.get_object('textview_webseeds').get_buffer() + lines = ( + textview_buf.get_text( + *textview_buf.get_bounds(), include_hidden_chars=False + ) + .strip() + .split('\n') + ) + webseeds = [] + for line in lines: + line = line.replace('\\', '/') # Fix any mistyped urls. + if is_url(line): + webseeds.append(line) + # Get the piece length in bytes + combo = self.builder.get_object('combo_piece_size') + piece_length = self.parse_piece_size_text( + combo.get_model()[combo.get_active()][0] + ) + + author = self.builder.get_object('entry_author').get_text() + comment = self.builder.get_object('entry_comments').get_text() + private = self.builder.get_object('chk_private_flag').get_active() + add_to_session = self.builder.get_object('chk_add_to_session').get_active() + + if is_remote: + + def torrent_created(): + self.builder.get_object('progress_dialog').hide() + client.deregister_event_handler( + 'CreateTorrentProgressEvent', on_create_torrent_progress_event + ) + + def on_create_torrent_progress_event(piece_count, num_pieces): + self._on_create_torrent_progress(piece_count, num_pieces) + if piece_count == num_pieces: + from twisted.internet import reactor + + reactor.callLater(0.5, torrent_created) + + client.register_event_handler( + 'CreateTorrentProgressEvent', on_create_torrent_progress_event + ) + + client.core.create_torrent( + decode_bytes(path), + tracker, + piece_length, + comment, + decode_bytes(result), + webseeds, + private, + author, + trackers, + add_to_session, + ) + + else: + + def hide_progress(result): + self.builder.get_object('progress_dialog').hide() + + deferToThread( + self.create_torrent, + decode_bytes(path), + tracker, + piece_length, + self._on_create_torrent_progress, + comment, + decode_bytes(result), + webseeds, + private, + author, + trackers, + add_to_session, + ).addCallback(hide_progress) + + # Setup progress dialog + self.builder.get_object('progress_dialog').set_transient_for( + component.get('MainWindow').window + ) + self.builder.get_object('progress_dialog').show_all() + + self.dialog.destroy() + + def create_torrent( + self, + path, + tracker, + piece_length, + progress, + comment, + target, + webseeds, + private, + created_by, + trackers, + add_to_session, + ): + import deluge.metafile + + deluge.metafile.make_meta_file( + path, + tracker, + piece_length, + progress=progress, + comment=comment, + target=target, + webseeds=webseeds, + private=private, + created_by=created_by, + trackers=trackers, + ) + + if add_to_session: + with open(target, 'rb') as _file: + filedump = b64encode(_file.read()) + client.core.add_torrent_file_async( + os.path.split(target)[-1], + filedump, + {'download_location': os.path.split(path)[0]}, + ) + + def _on_create_torrent_progress(self, value, num_pieces): + percent = value / num_pieces + + def update_pbar_with_gobject(percent): + pbar = self.builder.get_object('progressbar') + pbar.set_text('%.2f%%' % (percent * 100)) + pbar.set_fraction(percent) + return False + + if percent >= 0 and percent <= 1.0: + # Make sure there are no threads race conditions that can + # crash the UI while updating it. + idle_add(update_pbar_with_gobject, percent) + + def on_button_up_clicked(self, widget): + log.debug('on_button_up_clicked') + row = ( + self.builder.get_object('tracker_treeview') + .get_selection() + .get_selected()[1] + ) + if row is None: + return + if self.trackers_liststore[row][0] == 0: + return + else: + self.trackers_liststore[row][0] -= 1 + + def on_button_down_clicked(self, widget): + log.debug('on_button_down_clicked') + row = ( + self.builder.get_object('tracker_treeview') + .get_selection() + .get_selected()[1] + ) + if row is None: + return + self.trackers_liststore[row][0] += 1 + + def on_button_add_clicked(self, widget): + log.debug('on_button_add_clicked') + builder = Gtk.Builder() + builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'edit_trackers.add.ui') + ) + ) + dialog = builder.get_object('add_tracker_dialog') + dialog.set_transient_for(self.dialog) + textview = builder.get_object('textview_trackers') + if self.config['createtorrent.trackers']: + textview.get_buffer().set_text( + '\n'.join(self.config['createtorrent.trackers']) + ) + else: + textview.get_buffer().set_text('') + textview.grab_focus() + response = dialog.run() + + if response == Gtk.ResponseType.OK: + # Create a list of trackers from the textview buffer + textview_buf = textview.get_buffer() + trackers_text = textview_buf.get_text( + *textview_buf.get_bounds(), include_hidden_chars=False + ) + log.debug('Create torrent tracker lines: %s', trackers_text) + self.config['createtorrent.trackers'] = trackers_text.split('/n') + + # Append trackers liststore with unique trackers and tiers starting from last tier number. + last_tier, orig_trackers = last_tier_trackers_from_liststore( + self.trackers_liststore + ) + for tracker, tier in trackers_tiers_from_text(trackers_text).items(): + if tracker not in orig_trackers: + self.trackers_liststore.append([tier + last_tier, tracker]) + + dialog.destroy() + + def on_button_remove_clicked(self, widget): + log.debug('on_button_remove_clicked') + row = ( + self.builder.get_object('tracker_treeview') + .get_selection() + .get_selected()[1] + ) + if row is None: + return + self.trackers_liststore.remove(row) diff --git a/deluge/ui/gtk3/details_tab.py b/deluge/ui/gtk3/details_tab.py new file mode 100644 index 0000000..04a5eab --- /dev/null +++ b/deluge/ui/gtk3/details_tab.py @@ -0,0 +1,71 @@ +# +# Copyright (C) 2008 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. +# + +import logging +from xml.sax.saxutils import escape as xml_escape + +import deluge.component as component +from deluge.common import decode_bytes, fdate, fsize, is_url + +from .tab_data_funcs import fdate_or_dash, fpieces_num_size +from .torrentdetails import Tab + +log = logging.getLogger(__name__) + + +class DetailsTab(Tab): + def __init__(self): + super().__init__('Details', 'details_tab', 'details_tab_label') + + self.add_tab_widget('summary_name', None, ('name',)) + self.add_tab_widget('summary_total_size', fsize, ('total_size',)) + self.add_tab_widget('summary_num_files', str, ('num_files',)) + self.add_tab_widget('summary_completed', fdate_or_dash, ('completed_time',)) + self.add_tab_widget('summary_date_added', fdate, ('time_added',)) + self.add_tab_widget('summary_torrent_path', None, ('download_location',)) + self.add_tab_widget('summary_hash', str, ('hash',)) + self.add_tab_widget('summary_comments', str, ('comment',)) + self.add_tab_widget('summary_creator', str, ('creator',)) + self.add_tab_widget( + 'summary_pieces', fpieces_num_size, ('num_pieces', 'piece_length') + ) + + def update(self): + # Get the first selected torrent + selected = component.get('TorrentView').get_selected_torrents() + + # Only use the first torrent in the list or return if None selected + if selected: + selected = selected[0] + else: + # No torrent is selected in the torrentview + self.clear() + return + + session = component.get('SessionProxy') + session.get_torrent_status(selected, self.status_keys).addCallback( + self._on_get_torrent_status + ) + + def _on_get_torrent_status(self, status): + # Check to see if we got valid data from the core + if status is None: + return + + # Update all the label widgets + for widget in self.tab_widgets.values(): + txt = xml_escape(self.widget_status_as_fstr(widget, status)) + if decode_bytes(widget.obj.get_text()) != txt: + if 'comment' in widget.status_keys and is_url(txt): + widget.obj.set_markup(f'<a href="{txt}">{txt}</a>') + else: + widget.obj.set_markup(txt) + + def clear(self): + for widget in self.tab_widgets.values(): + widget.obj.set_text('') 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 diff --git a/deluge/ui/gtk3/edittrackersdialog.py b/deluge/ui/gtk3/edittrackersdialog.py new file mode 100644 index 0000000..861e392 --- /dev/null +++ b/deluge/ui/gtk3/edittrackersdialog.py @@ -0,0 +1,348 @@ +# +# Copyright (C) 2007, 2008 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. +# + +import logging +import os.path + +from gi.repository import Gdk, Gtk +from twisted.internet import defer + +import deluge.component as component +from deluge.common import is_url, resource_filename +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +from .common import get_deluge_icon + +log = logging.getLogger(__name__) + + +def last_tier_trackers_from_liststore(trackers_liststore): + """Create a list of tracker from existing liststore and find last tier number. + + Args: + tracker_liststore (Gtk.ListStore): A Gtk.ListStore with [tier (int), tracker (str)] rows. + + Returns: + tuple(int, list): A tuple containing last tier number and list of trackers. + + """ + + last_tier = 0 + trackers_from_liststore = [] + for tier, tracker in trackers_liststore: + trackers_from_liststore.append(tracker) + if tier >= last_tier: + last_tier = tier + 1 + + return last_tier, trackers_from_liststore + + +def trackers_tiers_from_text(text_str=''): + """Create a list of trackers from text. + + Any duplicate trackers are removed. + + Args: + text_input (str): A block of text with tracker separated by newlines. + + Returns: + list: The list of trackers. + + Notes: + Trackers should be separated by newlines and empty line denotes start of new tier. + + """ + + trackers = {} + tier = 0 + + lines = text_str.strip().split('\n') + for line in lines: + if not line: + tier += 1 + continue + line = line.replace('\\', '/') # Fix any mistyped urls. + if is_url(line) and line not in trackers: + trackers[line] = tier + + return trackers + + +class EditTrackersDialog: + def __init__(self, torrent_id, parent=None): + self.torrent_id = torrent_id + self.builder = Gtk.Builder() + self.gtkui_config = ConfigManager('gtk3ui.conf') + + # Main dialog + self.builder.add_from_file( + resource_filename(__package__, os.path.join('glade', 'edit_trackers.ui')) + ) + # add tracker dialog + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'edit_trackers.add.ui') + ) + ) + # edit tracker dialog + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'edit_trackers.edit.ui') + ) + ) + + self.dialog = self.builder.get_object('edit_trackers_dialog') + self.treeview = self.builder.get_object('tracker_treeview') + self.add_tracker_dialog = self.builder.get_object('add_tracker_dialog') + self.add_tracker_dialog.set_transient_for(self.dialog) + self.edit_tracker_entry = self.builder.get_object('edit_tracker_entry') + self.edit_tracker_entry.set_transient_for(self.dialog) + self.dialog.set_icon(get_deluge_icon()) + + self.load_edit_trackers_dialog_state() + + if parent is not None: + self.dialog.set_transient_for(parent) + + # Connect the signals + self.builder.connect_signals(self) + + # Create a liststore for tier, url + self.liststore = Gtk.ListStore(int, str) + + # Create the columns + self.treeview.append_column( + Gtk.TreeViewColumn(_('Tier'), Gtk.CellRendererText(), text=0) + ) + self.treeview.append_column( + Gtk.TreeViewColumn(_('Tracker'), Gtk.CellRendererText(), text=1) + ) + + self.treeview.set_model(self.liststore) + self.liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) + + self.dialog.connect('delete-event', self._on_delete_event) + self.dialog.connect('response', self._on_response) + self.treeview.connect('button_press_event', self.on_button_press_event) + + self.add_tracker_dialog.connect('key-press-event', self.on_key_add_press_event) + self.add_tracker_dialog.connect('delete-event', self.on_delete_event_add) + self.edit_tracker_entry.connect('key-press-event', self.on_key_edit_press_event) + self.edit_tracker_entry.connect('delete-event', self.on_delete_event_edit) + + def run(self): + # Make sure we have a torrent_id.. if not just return + if self.torrent_id is None: + return + + # Get the trackers for this torrent + session = component.get('SessionProxy') + session.get_torrent_status(self.torrent_id, ['trackers']).addCallback( + self._on_get_torrent_status + ) + client.force_call() + + self.deferred = defer.Deferred() + return self.deferred + + def __del__(self): + del self.gtkui_config + + def load_edit_trackers_dialog_state(self): + w = self.gtkui_config['edit_trackers_dialog_width'] + h = self.gtkui_config['edit_trackers_dialog_height'] + if w is not None and h is not None: + self.dialog.resize(w, h) + + def on_edit_trackers_dialog_configure_event(self, widget, event): + self.gtkui_config['edit_trackers_dialog_width'] = event.width + self.gtkui_config['edit_trackers_dialog_height'] = event.height + + def _on_delete_event(self, widget, event): + self.deferred.callback(Gtk.ResponseType.DELETE_EVENT) + self.dialog.destroy() + + def _on_response(self, widget, response): + if response == 1: + self.trackers = [] + + def each(model, path, _iter, data): + tracker = {} + tracker['tier'] = model.get_value(_iter, 0) + tracker['url'] = model.get_value(_iter, 1) + self.trackers.append(tracker) + + self.liststore.foreach(each, None) + if self.old_trackers != self.trackers: + # Set the torrens trackers + client.core.set_torrent_trackers(self.torrent_id, self.trackers) + self.deferred.callback(Gtk.ResponseType.OK) + else: + self.deferred.callback(Gtk.ResponseType.CANCEL) + else: + self.deferred.callback(Gtk.ResponseType.CANCEL) + self.dialog.destroy() + + def _on_get_torrent_status(self, status): + """Display trackers dialog""" + self.old_trackers = list(status['trackers']) + for tracker in self.old_trackers: + self.add_tracker(tracker['tier'], tracker['url']) + self.treeview.set_cursor(0) + self.dialog.show() + + def add_tracker(self, tier, url): + """Adds a tracker to the list""" + self.liststore.append([tier, url]) + + def get_selected(self): + """Returns the selected tracker""" + return self.treeview.get_selection().get_selected()[1] + + def on_button_add_clicked(self, widget): + log.debug('on_button_add_clicked') + # Show the add tracker dialog + self.add_tracker_dialog.show() + self.builder.get_object('textview_trackers').grab_focus() + self.dialog.set_sensitive(False) + + def on_button_remove_clicked(self, widget): + log.debug('on_button_remove_clicked') + selected = self.get_selected() + if selected is not None: + self.liststore.remove(selected) + + def on_button_edit_clicked(self, widget): + """edits an existing tracker on edit button click""" + log.debug('on_button_edit_clicked') + self._edit_tracker() + + def on_button_press_event(self, widget, event): + """edits an existing tracker on double click on tracker name""" + if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: + log.debug('button_press_event double click') + self._edit_tracker() + + def _edit_tracker(self): + """edits an existing tracker""" + selected = self.get_selected() + if selected: + tracker = self.liststore.get_value(selected, 1) + self.builder.get_object('entry_edit_tracker').set_text(tracker) + self.edit_tracker_entry.show() + self.edit_tracker_entry.grab_focus() + self.dialog.set_sensitive(False) + + def _close_edit_dialog(self): + self.dialog.set_sensitive(True) + self.edit_tracker_entry.hide() + + def on_button_edit_cancel_clicked(self, widget): + """handles the cancel button""" + log.debug('on_button_edit_cancel_clicked') + self._close_edit_dialog() + + def on_key_edit_press_event(self, widget, event): + """handles Escape key press""" + if event.keyval == Gdk.KEY_Escape: + log.debug('on_key_edit_press_event') + self._close_edit_dialog() + + def on_delete_event_edit(self, widget, event): + """handles the Top-Right X button""" + log.debug('on_delete_event_edit') + self._close_edit_dialog() + return True + + def on_button_edit_ok_clicked(self, widget): + log.debug('on_button_edit_ok_clicked') + selected = self.get_selected() + tracker = self.builder.get_object('entry_edit_tracker').get_text() + self.liststore.set_value(selected, 1, tracker) + self.dialog.set_sensitive(True) + self.edit_tracker_entry.hide() + + def on_button_up_clicked(self, widget): + log.debug('on_button_up_clicked') + selected = self.get_selected() + num_rows = self.liststore.iter_n_children(None) + if selected is not None and num_rows > 1: + tier = self.liststore.get_value(selected, 0) + if tier <= 0: + return + new_tier = tier - 1 + # Now change the tier for this tracker + self.liststore.set_value(selected, 0, new_tier) + + def on_button_down_clicked(self, widget): + log.debug('on_button_down_clicked') + selected = self.get_selected() + num_rows = self.liststore.iter_n_children(None) + if selected is not None and num_rows > 1: + tier = self.liststore.get_value(selected, 0) + new_tier = tier + 1 + # Now change the tier for this tracker + self.liststore.set_value(selected, 0, new_tier) + + def on_button_add_ok_clicked(self, widget): + log.debug('on_button_add_ok_clicked') + + # Create a list of trackers from the textview widget + textview_buf = self.builder.get_object('textview_trackers').get_buffer() + trackers_text = textview_buf.get_text( + *textview_buf.get_bounds(), include_hidden_chars=False + ) + + for tracker in trackers_tiers_from_text(trackers_text): + # Figure out what tier number to use.. it's going to be the highest+1 + # Also check for duplicates + # Check if there are any entries + duplicate = False + highest_tier = -1 + for row in self.liststore: + tier = row[0] + if tier > highest_tier: + highest_tier = tier + if tracker == row[1]: + duplicate = True + break + + # If not a duplicate, then add it to the list + if not duplicate: + # Add the tracker to the list + self.add_tracker(highest_tier + 1, tracker) + + # Clear the entry widget and hide the dialog + textview_buf.set_text('') + self.dialog.set_sensitive(True) + self.add_tracker_dialog.hide() + + def _discard_and_close_add_dialog(self): + # Clear the entry widget and hide the dialog + b = Gtk.TextBuffer() + self.builder.get_object('textview_trackers').set_buffer(b) + self.dialog.set_sensitive(True) + self.add_tracker_dialog.hide() + + def on_button_add_cancel_clicked(self, widget): + """handles the cancel button""" + log.debug('on_button_add_cancel_clicked') + self._discard_and_close_add_dialog() + + def on_key_add_press_event(self, widget, event): + """handles Escape key press""" + if event.keyval == Gdk.KEY_Escape: + log.debug('on_key_add_press_event') + self._discard_and_close_add_dialog() + + def on_delete_event_add(self, widget, event): + """handles the Top-Right X button""" + log.debug('on_delete_event_add') + self._discard_and_close_add_dialog() + return True diff --git a/deluge/ui/gtk3/files_tab.py b/deluge/ui/gtk3/files_tab.py new file mode 100644 index 0000000..6e0fba4 --- /dev/null +++ b/deluge/ui/gtk3/files_tab.py @@ -0,0 +1,863 @@ +# +# Copyright (C) 2008 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. +# + +import json +import logging +import os.path + +import gi # isort:skip (Required before Gtk import). + +gi.require_version('Gtk', '3.0') + +# isort:imports-thirdparty +from gi.repository import Gio, Gtk +from gi.repository.Gdk import DragAction, ModifierType, keyval_name +from gi.repository.GObject import TYPE_UINT64 + +# isort:imports-firstparty +import deluge.component as component +from deluge.common import open_file, show_file +from deluge.ui.client import client +from deluge.ui.common import FILE_PRIORITY + +# isort:imports-localfolder +from .common import ( + listview_replace_treestore, + load_pickled_state_file, + reparent_iter, + save_pickled_state_file, +) +from .torrentdetails import Tab +from .torrentview_data_funcs import cell_data_size + +log = logging.getLogger(__name__) + +CELL_PRIORITY_ICONS = { + FILE_PRIORITY['Skip']: 'action-unavailable-symbolic', + FILE_PRIORITY['Low']: 'go-down-symbolic', + FILE_PRIORITY['Normal']: 'go-next-symbolic', + FILE_PRIORITY['High']: 'go-up-symbolic', +} + +G_ICON_DIRECTORY = Gio.content_type_get_icon('inode/directory') + + +def cell_priority(column, cell, model, row, data): + if model.get_value(row, 5) == -1: + # This is a folder, so lets just set it blank for now + cell.set_property('text', '') + return + priority = model.get_value(row, data) + cell.set_property('text', _(FILE_PRIORITY[priority])) + + +def cell_priority_icon(column, cell, model, row, data): + if model.get_value(row, 5) == -1: + # This is a folder, so lets just set it blank for now + cell.set_property('icon-name', None) + return + priority = model.get_value(row, data) + cell.set_property('icon-name', CELL_PRIORITY_ICONS[priority]) + + +def cell_filename(column, cell, model, row, data): + """Only show the tail portion of the file path""" + filepath = model.get_value(row, data) + cell.set_property('text', os.path.split(filepath)[1]) + + +def cell_progress(column, cell, model, row, data): + text = model.get_value(row, data[0]) + value = model.get_value(row, data[1]) + cell.set_property('visible', True) + cell.set_property('text', text) + cell.set_property('value', value) + + +class FilesTab(Tab): + def __init__(self): + super().__init__('Files', 'files_tab', 'files_tab_label') + + self.listview = self.main_builder.get_object('files_listview') + # filename, size, progress string, progress value, priority, file index, icon id + self.treestore = Gtk.TreeStore(str, TYPE_UINT64, str, float, int, int, Gio.Icon) + self.treestore.set_sort_column_id(0, Gtk.SortType.ASCENDING) + + # We need to store the row that's being edited to prevent updating it until + # it's been done editing + self._editing_index = None + + # Filename column + self.filename_column_name = _('Filename') + column = Gtk.TreeViewColumn(self.filename_column_name) + render = Gtk.CellRendererPixbuf() + column.pack_start(render, False) + column.add_attribute(render, 'gicon', 6) + render = Gtk.CellRendererText() + render.set_property('editable', True) + render.connect('edited', self._on_filename_edited) + render.connect('editing-started', self._on_filename_editing_start) + render.connect('editing-canceled', self._on_filename_editing_canceled) + column.pack_start(render, True) + column.add_attribute(render, 'text', 0) + column.set_sort_column_id(0) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(200) + column.set_reorderable(True) + self.listview.append_column(column) + + # Size column + column = Gtk.TreeViewColumn(_('Size')) + render = Gtk.CellRendererText() + column.pack_start(render, False) + column.set_cell_data_func(render, cell_data_size, 1) + column.set_sort_column_id(1) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(50) + column.set_reorderable(True) + self.listview.append_column(column) + + # Progress column + column = Gtk.TreeViewColumn(_('Progress')) + render = Gtk.CellRendererProgress() + render.set_padding(0, 1) + column.pack_start(render, True) + column.set_cell_data_func(render, cell_progress, (2, 3)) + column.set_sort_column_id(3) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + # Priority column + column = Gtk.TreeViewColumn(_('Priority')) + render = Gtk.CellRendererPixbuf() + column.pack_start(render, False) + column.set_cell_data_func(render, cell_priority_icon, 4) + render = Gtk.CellRendererText() + column.pack_start(render, False) + column.set_cell_data_func(render, cell_priority, 4) + column.set_sort_column_id(4) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + # Bugfix: Last column needs max_width set to stop scrollbar appearing + column.set_max_width(200) + column.set_reorderable(True) + self.listview.append_column(column) + + self.listview.set_model(self.treestore) + + self.listview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + + self.file_menu = self.main_builder.get_object('menu_file_tab') + self.file_menu_priority_items = [ + self.main_builder.get_object('menuitem_skip'), + self.main_builder.get_object('menuitem_low'), + self.main_builder.get_object('menuitem_normal'), + self.main_builder.get_object('menuitem_high'), + self.main_builder.get_object('menuitem_priority_sep'), + ] + + self.localhost_widgets = [ + self.main_builder.get_object('menuitem_open_file'), + self.main_builder.get_object('menuitem_show_file'), + self.main_builder.get_object('menuitem3'), + ] + + self.listview.connect('row-activated', self._on_row_activated) + self.listview.connect('key-press-event', self._on_key_press_event) + self.listview.connect('button-press-event', self._on_button_press_event) + + self.listview.enable_model_drag_source( + ModifierType.BUTTON1_MASK, + [('text/plain', 0, 0)], + DragAction.DEFAULT | DragAction.MOVE, + ) + self.listview.enable_model_drag_dest([('text/plain', 0, 0)], DragAction.DEFAULT) + + self.listview.connect('drag_data_get', self._on_drag_data_get_data) + self.listview.connect('drag_data_received', self._on_drag_data_received_data) + + component.get('MainWindow').connect_signals(self) + + # Connect to various events from the daemon + client.register_event_handler( + 'TorrentFileRenamedEvent', self._on_torrentfilerenamed_event + ) + client.register_event_handler( + 'TorrentFolderRenamedEvent', self._on_torrentfolderrenamed_event + ) + client.register_event_handler( + 'TorrentRemovedEvent', self._on_torrentremoved_event + ) + + # Attempt to load state + self.load_state() + + # torrent_id: (filepath, size) + self.files_list = {} + + self.torrent_id = None + + def start(self): + attr = 'hide' if not client.is_localhost() else 'show' + for widget in self.localhost_widgets: + getattr(widget, attr)() + + def save_state(self): + # Get the current sort order of the view + column_id, sort_order = self.treestore.get_sort_column_id() + + # Setup state dict + state = { + 'columns': {}, + 'sort_id': int(column_id) if column_id >= 0 else None, + 'sort_order': int(sort_order) if sort_order >= 0 else None, + } + + for index, column in enumerate(self.listview.get_columns()): + state['columns'][column.get_title()] = { + 'position': index, + 'width': column.get_width(), + } + + save_pickled_state_file('files_tab.state', state) + + def load_state(self): + state = load_pickled_state_file('files_tab.state') + + if not state: + return + + if state['sort_id'] is not None and state['sort_order'] is not None: + self.treestore.set_sort_column_id(state['sort_id'], state['sort_order']) + + for index, column in enumerate(self.listview.get_columns()): + cname = column.get_title() + if cname in state['columns']: + cstate = state['columns'][cname] + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + column.set_fixed_width(cstate['width'] if cstate['width'] > 0 else 10) + if state['sort_id'] == index and state['sort_order'] is not None: + column.set_sort_indicator(True) + column.set_sort_order(state['sort_order']) + if cstate['position'] != index: + # Column is in wrong position + if cstate['position'] == 0: + self.listview.move_column_after(column, None) + elif ( + self.listview.get_columns()[cstate['position'] - 1].get_title() + != cname + ): + self.listview.move_column_after( + column, self.listview.get_columns()[cstate['position'] - 1] + ) + + def update(self): + # Get the first selected torrent + torrent_id = component.get('TorrentView').get_selected_torrents() + + # Only use the first torrent in the list or return if None selected + if len(torrent_id) != 0: + torrent_id = torrent_id[0] + else: + # No torrent is selected in the torrentview + self.clear() + return + + status_keys = ['file_progress', 'file_priorities'] + if torrent_id != self.torrent_id: + # We only want to do this if the torrent_id has changed + self.treestore.clear() + self.torrent_id = torrent_id + status_keys += ['storage_mode', 'is_seed'] + + if self.torrent_id in self.files_list: + # We already have the files list stored, so just update the view + self.update_files() + + if ( + self.torrent_id not in self.files_list + or not self.files_list[self.torrent_id] + ): + # We need to get the files list + log.debug('Getting file list from core..') + status_keys += ['files'] + + component.get('SessionProxy').get_torrent_status( + self.torrent_id, status_keys + ).addCallback(self._on_get_torrent_status, self.torrent_id) + + def clear(self): + self.treestore.clear() + self.torrent_id = None + + def _on_row_activated(self, tree, path, view_column): + self.on_menuitem_open_file_activate(None) + + def get_file_path(self, row, path=''): + if not row: + return path + + path = self.treestore.get_value(row, 0) + path + return self.get_file_path(self.treestore.iter_parent(row), path) + + def _on_open_file(self, status): + paths = self.listview.get_selection().get_selected_rows()[1] + selected = [] + for path in paths: + selected.append(self.treestore.get_iter(path)) + + for select in selected: + path = self.get_file_path(select).split('/') + filepath = os.path.join(status['download_location'], *path) + log.debug('Open file: %s', filepath) + timestamp = component.get('MainWindow').get_timestamp() + open_file(filepath, timestamp=timestamp) + + def _on_show_file(self, status): + paths = self.listview.get_selection().get_selected_rows()[1] + selected = [] + for path in paths: + selected.append(self.treestore.get_iter(path)) + + for select in selected: + path = self.get_file_path(select).split('/') + filepath = os.path.join(status['download_location'], *path) + log.debug('Show file: %s', filepath) + timestamp = component.get('MainWindow').get_timestamp() + show_file(filepath, timestamp=timestamp) + + # The following 3 methods create the folder/file view in the treeview + def prepare_file_store(self, torrent_files): + split_files = {} + for index, torrent_file in enumerate(torrent_files): + self.prepare_file(torrent_file, torrent_file['path'], index, split_files) + self.add_files(None, split_files) + + def prepare_file(self, torrent_file, file_name, file_num, files_storage): + first_slash_index = file_name.find('/') + if first_slash_index == -1: + files_storage[file_name] = (file_num, torrent_file) + else: + file_name_chunk = file_name[: first_slash_index + 1] + if file_name_chunk not in files_storage: + files_storage[file_name_chunk] = {} + self.prepare_file( + torrent_file, + file_name[first_slash_index + 1 :], + file_num, + files_storage[file_name_chunk], + ) + + def add_files(self, parent_iter, split_files): + chunk_size_total = 0 + for key, value in split_files.items(): + if key.endswith('/'): + chunk_iter = self.treestore.append( + parent_iter, [key, 0, '', 0, 0, -1, G_ICON_DIRECTORY] + ) + chunk_size = self.add_files(chunk_iter, value) + self.treestore.set(chunk_iter, 1, chunk_size) + chunk_size_total += chunk_size + else: + mime_type, uncertain = Gio.content_type_guess(key, None) + if not uncertain and mime_type: + mime_icon = Gio.content_type_get_symbolic_icon(mime_type) + else: + mime_icon = Gio.content_type_get_symbolic_icon('text/plain') + self.treestore.append( + parent_iter, [key, value[1]['size'], '', 0, 0, value[0], mime_icon] + ) + chunk_size_total += value[1]['size'] + return chunk_size_total + + def update_files(self): + with listview_replace_treestore(self.listview): + self.prepare_file_store(self.files_list[self.torrent_id]) + root = Gtk.TreePath.new_first() + self.listview.expand_row(root, False) + + def get_selected_files(self): + """Returns a list of file indexes that are selected.""" + + def get_iter_children(itr, selected): + i = self.treestore.iter_children(itr) + while i: + selected.append(self.treestore[i][5]) + if self.treestore.iter_has_child(i): + get_iter_children(i, selected) + i = self.treestore.iter_next(i) + + selected = [] + paths = self.listview.get_selection().get_selected_rows()[1] + for path in paths: + i = self.treestore.get_iter(path) + selected.append(self.treestore[i][5]) + if self.treestore.iter_has_child(i): + get_iter_children(i, selected) + + return selected + + def get_files_from_tree(self, rows, files_list, indent): + if not rows: + return None + + for row in rows: + if row[5] > -1: + files_list.append((row[5], row)) + self.get_files_from_tree(row.iterchildren(), files_list, indent + 1) + return None + + def update_folder_percentages(self): + """Go through the tree and update the folder complete percentages.""" + root = self.treestore.get_iter_first() + if root is None or self.treestore[root][5] != -1: + return + + def get_completed_bytes(row): + completed_bytes = 0 + parent = self.treestore.iter_parent(row) + while row: + if self.treestore.iter_children(row): + completed_bytes += get_completed_bytes( + self.treestore.iter_children(row) + ) + else: + completed_bytes += ( + self.treestore[row][1] * self.treestore[row][3] / 100 + ) + + row = self.treestore.iter_next(row) + + try: + value = completed_bytes / self.treestore[parent][1] * 100 + except ZeroDivisionError: + # Catch the unusual error found when moving folders around + value = 0 + self.treestore[parent][3] = value + self.treestore[parent][2] = '%i%%' % value + return completed_bytes + + get_completed_bytes(self.treestore.iter_children(root)) + + def _on_get_torrent_status(self, status, torrent_id): + # Check stored torrent id matches the callback id + if self.torrent_id != torrent_id: + return + + if 'is_seed' in status: + self.__is_seed = status['is_seed'] + + if 'files' in status: + self.files_list[self.torrent_id] = status['files'] + self.update_files() + + # (index, iter) + files_list = [] + self.get_files_from_tree(self.treestore, files_list, 0) + files_list.sort() + for index, row in files_list: + # Do not update a row that is being edited + if self._editing_index == row[5]: + continue + + try: + progress_string = '%i%%' % (status['file_progress'][index] * 100) + except IndexError: + continue + if row[2] != progress_string: + row[2] = progress_string + progress_value = status['file_progress'][index] * 100 + if row[3] != progress_value: + row[3] = progress_value + file_priority = status['file_priorities'][index] + if row[4] != file_priority: + row[4] = file_priority + if self._editing_index != -1: + # Only update if no folder is being edited + self.update_folder_percentages() + + def _on_button_press_event(self, widget, event): + """This is a callback for showing the right-click context menu.""" + log.debug('on_button_press_event') + # We only care about right-clicks + if event.button == 3: + x, y = event.get_coords() + cursor_path = self.listview.get_path_at_pos(int(x), int(y)) + if not cursor_path: + return + + paths = self.listview.get_selection().get_selected_rows()[1] + if cursor_path[0] not in paths: + row = self.treestore.get_iter(cursor_path[0]) + self.listview.get_selection().unselect_all() + self.listview.get_selection().select_iter(row) + + for widget in self.file_menu_priority_items: + widget.set_sensitive(not self.__is_seed) + + self.file_menu.popup(None, None, None, None, event.button, event.time) + return True + + def _on_key_press_event(self, widget, event): + keyname = keyval_name(event.keyval) + if keyname is not None: + func = getattr(self, 'keypress_' + keyname.lower(), None) + selected_rows = self.listview.get_selection().get_selected_rows()[1] + if func and selected_rows: + return func(event) + + def keypress_menu(self, event): + self.file_menu.popup(None, None, None, None, 3, event.time) + return True + + def keypress_f2(self, event): + path, col = self.listview.get_cursor() + for column in self.listview.get_columns(): + if column.get_title() == self.filename_column_name: + self.listview.set_cursor(path, column, True) + return True + + def on_menuitem_open_file_activate(self, menuitem): + if client.is_localhost: + component.get('SessionProxy').get_torrent_status( + self.torrent_id, ['download_location'] + ).addCallback(self._on_open_file) + + def on_menuitem_show_file_activate(self, menuitem): + if client.is_localhost: + component.get('SessionProxy').get_torrent_status( + self.torrent_id, ['download_location'] + ).addCallback(self._on_show_file) + + def _set_file_priorities_on_user_change(self, selected, priority): + """Sets the file priorities in the core. It will change the selected with the 'priority'""" + file_priorities = [] + + def set_file_priority(model, path, _iter, data): + index = model.get_value(_iter, 5) + if index in selected and index != -1: + file_priorities.append((index, priority)) + elif index != -1: + file_priorities.append((index, model.get_value(_iter, 4))) + + self.treestore.foreach(set_file_priority, None) + file_priorities.sort() + priorities = [p[1] for p in file_priorities] + log.debug('priorities: %s', priorities) + client.core.set_torrent_options( + [self.torrent_id], {'file_priorities': priorities} + ) + + def on_menuitem_skip_activate(self, menuitem): + self._set_file_priorities_on_user_change( + self.get_selected_files(), FILE_PRIORITY['Skip'] + ) + + def on_menuitem_low_activate(self, menuitem): + self._set_file_priorities_on_user_change( + self.get_selected_files(), FILE_PRIORITY['Low'] + ) + + def on_menuitem_normal_activate(self, menuitem): + self._set_file_priorities_on_user_change( + self.get_selected_files(), FILE_PRIORITY['Normal'] + ) + + def on_menuitem_high_activate(self, menuitem): + self._set_file_priorities_on_user_change( + self.get_selected_files(), FILE_PRIORITY['High'] + ) + + def on_menuitem_expand_all_activate(self, menuitem): + self.listview.expand_all() + + def _on_filename_edited(self, renderer, path, new_text): + index = self.treestore[path][5] + log.debug('new_text: %s', new_text) + + # Don't do anything if the text hasn't changed + if new_text == self.treestore[path][0]: + self._editing_index = None + return + + if index > -1: + # We are renaming a file + itr = self.treestore.get_iter(path) + # Recurse through the treestore to get the actual path of the file + + def get_filepath(i): + ip = self.treestore.iter_parent(i) + fp = '' + while ip: + fp = self.treestore[ip][0] + fp + ip = self.treestore.iter_parent(ip) + return fp + + # Only recurse if file is in a folder.. + if self.treestore.iter_parent(itr): + filepath = get_filepath(itr) + new_text + else: + filepath = new_text + + log.debug('filepath: %s', filepath) + + client.core.rename_files(self.torrent_id, [(index, filepath)]) + else: + # We are renaming a folder + folder = self.treestore[path][0] + + parent_path = '' + itr = self.treestore.iter_parent(self.treestore.get_iter(path)) + while itr: + parent_path = self.treestore[itr][0] + parent_path + itr = self.treestore.iter_parent(itr) + + client.core.rename_folder( + self.torrent_id, parent_path + folder, parent_path + new_text + ) + + self._editing_index = None + + def _on_filename_editing_start(self, renderer, editable, path): + self._editing_index = self.treestore[path][5] + + def _on_filename_editing_canceled(self, renderer): + self._editing_index = None + + def _on_torrentfilerenamed_event(self, torrent_id, index, name): + log.debug('index: %s name: %s', index, name) + + if torrent_id not in self.files_list: + return + + old_name = self.files_list[torrent_id][index]['path'] + self.files_list[torrent_id][index]['path'] = name + + # We need to update the filename displayed if we're currently viewing + # this torrents files. + if torrent_id != self.torrent_id: + return + + old_name_parent = old_name.split('/')[:-1] + parent_path = name.split('/')[:-1] + + if old_name_parent != parent_path: + if parent_path: + for i, p in enumerate(parent_path): + p_itr = self.get_iter_at_path('/'.join(parent_path[: i + 1]) + '/') + if not p_itr: + p_itr = self.get_iter_at_path('/'.join(parent_path[:i]) + '/') + p_itr = self.treestore.append( + p_itr, + [parent_path[i] + '/', 0, '', 0, 0, -1, G_ICON_DIRECTORY], + ) + p_itr = self.get_iter_at_path('/'.join(parent_path) + '/') + old_name_itr = self.get_iter_at_path(old_name) + self.treestore.append( + p_itr, + self.treestore.get( + old_name_itr, *range(self.treestore.get_n_columns()) + ), + ) + self.treestore.remove(old_name_itr) + + # Remove old parent path + p_itr = self.get_iter_at_path('/'.join(old_name_parent) + '/') + self.remove_childless_folders(p_itr) + else: + new_folders = name.split('/')[:-1] + parent_iter = None + for f in new_folders: + parent_iter = self.treestore.append( + parent_iter, [f + '/', 0, '', 0, 0, -1, G_ICON_DIRECTORY] + ) + child = self.get_iter_at_path(old_name) + self.treestore.append( + parent_iter, + self.treestore.get(child, *range(self.treestore.get_n_columns())), + ) + self.treestore.remove(child) + + else: + # This is just changing a filename without any folder changes + def set_file_name(model, path, itr, user_data): + if model[itr][5] == index: + model[itr][0] = os.path.split(name)[-1] + return True + + self.treestore.foreach(set_file_name, None) + + def get_iter_at_path(self, filepath): + """Returns the gtkTreeIter for filepath.""" + log.debug('get_iter_at_path: %s', filepath) + is_dir = False + if filepath[-1] == '/': + is_dir = True + + filepath = filepath.split('/') + if '' in filepath: + filepath.remove('') + + path_iter = None + itr = self.treestore.iter_children(None) + level = 0 + while itr: + ipath = self.treestore[itr][0] + if (level + 1) != len(filepath) and ipath == filepath[level] + '/': + # We're not at the last index, but we do have a match + itr = self.treestore.iter_children(itr) + level += 1 + continue + elif (level + 1) == len(filepath) and ipath == ( + filepath[level] + '/' if is_dir else filepath[level] + ): + # This is the iter we've been searching for + path_iter = itr + break + else: + itr = self.treestore.iter_next(itr) + continue + return path_iter + + def remove_childless_folders(self, itr): + """Goes up the tree removing childless itrs starting at itr.""" + while not self.treestore.iter_children(itr): + parent = self.treestore.iter_parent(itr) + self.treestore.remove(itr) + itr = parent + + def _on_torrentfolderrenamed_event(self, torrent_id, old_folder, new_folder): + log.debug('on_torrent_folder_renamed_signal') + log.debug('old_folder: %s new_folder: %s', old_folder, new_folder) + + if torrent_id not in self.files_list: + return + + if old_folder[-1] != '/': + old_folder += '/' + + if len(new_folder) > 0 and new_folder[-1] != '/': + new_folder += '/' + + for fd in self.files_list[torrent_id]: + if fd['path'].startswith(old_folder): + fd['path'] = fd['path'].replace(old_folder, new_folder, 1) + + if torrent_id == self.torrent_id: + old_split = old_folder.split('/') + try: + old_split.remove('') + except ValueError: + pass + + new_split = new_folder.split('/') + try: + new_split.remove('') + except ValueError: + pass + + old_folder_iter = self.get_iter_at_path(old_folder) + old_folder_iter_parent = self.treestore.iter_parent(old_folder_iter) + + new_folder_iter = self.get_iter_at_path(new_folder) if new_folder else None + + if len(new_split) == len(old_split): + # These are at the same tree depth, so it's a simple rename + self.treestore[old_folder_iter][0] = new_split[-1] + '/' + return + if new_folder_iter: + # This means that a folder by this name already exists + reparent_iter( + self.treestore, + self.treestore.iter_children(old_folder_iter), + new_folder_iter, + ) + else: + parent = old_folder_iter_parent + if new_split: + for ns in new_split[:-1]: + parent = self.treestore.append( + parent, [ns + '/', 0, '', 0, 0, -1, G_ICON_DIRECTORY] + ) + + self.treestore[old_folder_iter][0] = new_split[-1] + '/' + reparent_iter(self.treestore, old_folder_iter, parent) + else: + child_itr = self.treestore.iter_children(old_folder_iter) + reparent_iter( + self.treestore, + child_itr, + old_folder_iter_parent, + move_siblings=True, + ) + + # We need to check if the old_folder_iter no longer has children + # and if so, we delete it + self.remove_childless_folders(old_folder_iter) + + def _on_torrentremoved_event(self, torrent_id): + if torrent_id in self.files_list: + del self.files_list[torrent_id] + + def _on_drag_data_get_data(self, treeview, context, selection, target_id, etime): + paths = self.listview.get_selection().get_selected_rows()[1] + selection.set_text(json.dumps([str(path) for path in paths]), -1) + + def _on_drag_data_received_data( + self, treeview, context, x, y, selection, info, etime + ): + try: + selected = json.loads(selection.get_data()) + except TypeError: + log.debug('Invalid selection data: %s', selection.get_data()) + return + log.debug('selection.data: %s', selected) + drop_info = treeview.get_dest_row_at_pos(x, y) + model = treeview.get_model() + if drop_info: + itr = model.get_iter(drop_info[0]) + parent_iter = model.iter_parent(itr) + parent_path = '' + if model[itr][5] == -1: + parent_path += model[itr][0] + + while parent_iter: + parent_path = model[parent_iter][0] + parent_path + parent_iter = model.iter_parent(parent_iter) + + if model[selected[0]][5] == -1: + log.debug('parent_path: %s', parent_path) + log.debug('rename_to: %s', parent_path + model[selected[0]][0]) + # Get the full path of the folder we want to rename + pp = '' + itr = self.treestore.iter_parent(self.treestore.get_iter(selected[0])) + while itr: + pp = self.treestore[itr][0] + pp + itr = self.treestore.iter_parent(itr) + client.core.rename_folder( + self.torrent_id, + pp + model[selected[0]][0], + parent_path + model[selected[0]][0], + ) + else: + # [(index, filepath), ...] + to_rename = [] + for s in selected: + to_rename.append((model[s][5], parent_path + model[s][0])) + log.debug('to_rename: %s', to_rename) + client.core.rename_files(self.torrent_id, to_rename) diff --git a/deluge/ui/gtk3/filtertreeview.py b/deluge/ui/gtk3/filtertreeview.py new file mode 100644 index 0000000..40752d7 --- /dev/null +++ b/deluge/ui/gtk3/filtertreeview.py @@ -0,0 +1,370 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# 2008 Andrew Resch <andrewresch@gmail.com> +# 2014 Calum Lind <calumlind@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. +# + +import logging +import os +import warnings + +from gi.repository import Gtk +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository.Pango import EllipsizeMode + +import deluge.component as component +from deluge.common import TORRENT_STATE, decode_bytes, resource_filename +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +from .common import get_pixbuf + +log = logging.getLogger(__name__) + +STATE_PIX = { + 'All': 'all', + 'Downloading': 'downloading', + 'Seeding': 'seeding', + 'Paused': 'inactive', + 'Checking': 'checking', + 'Queued': 'queued', + 'Error': 'alert', + 'Active': 'active', + 'Allocating': 'checking', + 'Moving': 'checking', +} + +TRACKER_PIX = {'All': 'tracker_all', 'Error': 'tracker_warning'} + +FILTER_COLUMN = 5 + + +class FilterTreeView(component.Component): + def __init__(self): + component.Component.__init__(self, 'FilterTreeView', interval=2) + self.config = ConfigManager('gtk3ui.conf') + + self.tracker_icons = component.get('TrackerIcons') + + self.sidebar = component.get('SideBar') + self.treeview = Gtk.TreeView() + self.sidebar.add_tab(self.treeview, 'filters', 'Filters') + + # set filter to all when hidden: + self.sidebar.notebook.connect('hide', self._on_hide) + + # Create the treestore + # cat, value, label, count, pixmap, visible + self.treestore = Gtk.TreeStore(str, str, str, int, Pixbuf, bool) + + # Create the column and cells + column = Gtk.TreeViewColumn('Filters') + column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) + # icon cell + self.cell_pix = Gtk.CellRendererPixbuf() + column.pack_start(self.cell_pix, expand=False) + column.add_attribute(self.cell_pix, 'pixbuf', 4) + # label cell + cell_label = Gtk.CellRendererText() + cell_label.set_property('ellipsize', EllipsizeMode.END) + column.pack_start(cell_label, expand=True) + column.set_cell_data_func(cell_label, self.render_cell_data, None) + # count cell + self.cell_count = Gtk.CellRendererText() + self.cell_count.set_property('xalign', 1.0) + self.cell_count.set_padding(3, 0) + column.pack_start(self.cell_count, expand=False) + + self.treeview.append_column(column) + + # Style + self.treeview.set_show_expanders(True) + self.treeview.set_headers_visible(False) + self.treeview.set_level_indentation(-21) + # Force theme to use expander-size so we don't cut out entries due to indentation hack. + provider = Gtk.CssProvider() + provider.load_from_data(b'* {-GtkTreeView-expander-size: 9;}') + context = self.treeview.get_style_context() + context.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + self.treeview.set_model(self.treestore) + self.treeview.get_selection().connect('changed', self.on_selection_changed) + self.create_model_filter() + + self.treeview.connect('button-press-event', self.on_button_press_event) + + # filtertree menu + builder = Gtk.Builder() + builder.add_from_file( + resource_filename(__package__, os.path.join('glade', 'filtertree_menu.ui')) + ) + self.menu = builder.get_object('filtertree_menu') + builder.connect_signals(self) + + self.default_menu_items = self.menu.get_children() + + # add Cat nodes: + self.cat_nodes = {} + self.filters = {} + + def start(self): + self.cat_nodes = {} + self.filters = {} + # initial order of state filter: + self.cat_nodes['state'] = self.treestore.append( + None, ['cat', 'state', _('States'), 0, None, False] + ) + for state in ['All', 'Active'] + TORRENT_STATE: + self.update_row('state', state, 0, _(state)) + + self.cat_nodes['tracker_host'] = self.treestore.append( + None, ['cat', 'tracker_host', _('Trackers'), 0, None, False] + ) + self.update_row('tracker_host', 'All', 0, _('All')) + self.update_row('tracker_host', 'Error', 0, _('Error')) + self.update_row('tracker_host', '', 0, _('None')) + + self.cat_nodes['owner'] = self.treestore.append( + None, ['cat', 'owner', _('Owner'), 0, None, False] + ) + self.update_row('owner', 'localclient', 0, _('Admin')) + self.update_row('owner', '', 0, _('None')) + + # We set to this expand the rows on start-up + self.expand_rows = True + + self.selected_path = None + + def stop(self): + self.treestore.clear() + + def create_model_filter(self): + self.model_filter = self.treestore.filter_new() + self.model_filter.set_visible_column(FILTER_COLUMN) + self.treeview.set_model(self.model_filter) + + def cb_update_filter_tree(self, filter_items): + # create missing cat_nodes + for cat in filter_items: + if cat not in self.cat_nodes: + label = _(cat) + if cat == 'label': + label = _('Labels') + self.cat_nodes[cat] = self.treestore.append( + None, ['cat', cat, label, 0, None, False] + ) + + # update rows + visible_filters = [] + for cat, filters in filter_items.items(): + for value, count in filters: + self.update_row(cat, value, count) + visible_filters.append((cat, value)) + + # hide root-categories not returned by core-part of the plugin. + for cat in self.cat_nodes: + self.treestore.set_value( + self.cat_nodes[cat], + FILTER_COLUMN, + True if cat in filter_items else False, + ) + + # hide items not returned by core-plugin. + for f in self.filters: + if f not in visible_filters: + self.treestore.set_value(self.filters[f], FILTER_COLUMN, False) + + if self.expand_rows: + self.treeview.expand_all() + self.expand_rows = False + + if not self.selected_path: + self.select_default_filter() + + def update_row(self, cat, value, count, label=None): + def on_get_icon(icon): + if icon: + self.set_row_image(cat, value, icon.get_filename()) + + if (cat, value) in self.filters: + row = self.filters[(cat, value)] + self.treestore.set_value(row, 3, count) + else: + pix = self.get_pixmap(cat, value) + + if value == '': + if cat == 'label': + label = _('No Label') + elif cat == 'owner': + label = _('No Owner') + elif not label and value: + label = _(value) + + row = self.treestore.append( + self.cat_nodes[cat], [cat, value, label, count, pix, True] + ) + self.filters[(cat, value)] = row + + if cat == 'tracker_host' and value not in ('All', 'Error') and value: + d = self.tracker_icons.fetch(value) + d.addCallback(on_get_icon) + + self.treestore.set_value(row, FILTER_COLUMN, True) + return row + + def render_cell_data(self, column, cell, model, row, data): + cat = model.get_value(row, 0) + label = decode_bytes(model.get_value(row, 2)) + count = model.get_value(row, 3) + + # Suppress Warning: g_object_set_qdata: assertion `G_IS_OBJECT (object)' failed + original_filters = warnings.filters[:] + warnings.simplefilter('ignore') + try: + pix = model.get_value(row, 4) + finally: + warnings.filters = original_filters + + self.cell_pix.set_property('visible', True if pix else False) + + if cat == 'cat': + self.cell_count.set_property('visible', False) + cell.set_padding(10, 2) + label = '<b>%s</b>' % label + else: + count_txt = '<small>%s</small>' % count + self.cell_count.set_property('markup', count_txt) + self.cell_count.set_property('visible', True) + cell.set_padding(2, 1) + cell.set_property('markup', label) + + def get_pixmap(self, cat, value): + pix = None + if cat == 'state': + pix = STATE_PIX.get(value, None) + elif cat == 'tracker_host': + pix = TRACKER_PIX.get(value, None) + + if pix: + return get_pixbuf('%s16.png' % pix) + + def set_row_image(self, cat, value, filename): + pix = get_pixbuf(filename, size=16) + row = self.filters[(cat, value)] + self.treestore.set_value(row, 4, pix) + return False + + def on_selection_changed(self, selection): + try: + (model, row) = self.treeview.get_selection().get_selected() + if not row: + log.debug('nothing selected') + return + + cat = model.get_value(row, 0) + value = model.get_value(row, 1) + + filter_dict = {cat: [value]} + if value == 'All' or cat == 'cat': + filter_dict = {} + + component.get('TorrentView').set_filter(filter_dict) + + self.selected_path = model.get_path(row) + + except Exception as ex: + log.debug(ex) + # paths is likely None .. so lets return None + return None + + def update(self): + try: + hide_cat = [] + if not self.config['sidebar_show_trackers']: + hide_cat.append('tracker_host') + if not self.config['sidebar_show_owners']: + hide_cat.append('owner') + client.core.get_filter_tree( + self.config['sidebar_show_zero'], hide_cat + ).addCallback(self.cb_update_filter_tree) + except Exception as ex: + log.debug(ex) + + # Callbacks # + def on_button_press_event(self, widget, event): + """This is a callback for showing the right-click context menu.""" + x, y = event.get_coords() + path = self.treeview.get_path_at_pos(int(x), int(y)) + if not path: + return + path = path[0] + cat = self.model_filter[path][0] + + if event.button == 1: + # Prevent selecting a category label + if cat == 'cat': + if self.treeview.row_expanded(path): + self.treeview.collapse_row(path) + else: + self.treeview.expand_row(path, False) + if not self.selected_path: + self.select_default_filter() + else: + self.treeview.get_selection().select_path(self.selected_path) + return True + + elif event.button == 3: + # assign current cat, value to self: + x, y = event.get_coords() + path = self.treeview.get_path_at_pos(int(x), int(y)) + if not path: + return + row = self.model_filter.get_iter(path[0]) + self.cat = self.model_filter.get_value(row, 0) + self.value = self.model_filter.get_value(row, 1) + self.count = self.model_filter.get_value(row, 3) + + # Show the pop-up menu + self.set_menu_sensitivity() + self.menu.hide() + self.menu.popup(None, None, None, None, event.button, event.time) + self.menu.show() + + if cat == 'cat': + # Do not select the row + return True + + def set_menu_sensitivity(self): + # select-all/pause/resume + sensitive = self.cat != 'cat' and self.count != 0 + for item in self.default_menu_items: + item.set_sensitive(sensitive) + + def select_all(self): + """For use in popup menu.""" + component.get('TorrentView').treeview.get_selection().select_all() + + def on_select_all(self, event): + self.select_all() + + def on_pause_all(self, event): + self.select_all() + func = getattr(component.get('MenuBar'), 'on_menuitem_%s_activate' % 'pause') + func(event) + + def on_resume_all(self, event): + self.select_all() + func = getattr(component.get('MenuBar'), 'on_menuitem_%s_activate' % 'resume') + func(event) + + def _on_hide(self, *args): + self.select_default_filter() + + def select_default_filter(self): + row = self.filters[('state', 'All')] + path = self.treestore.get_path(row) + self.treeview.get_selection().select_path(path) diff --git a/deluge/ui/gtk3/glade/add_torrent_dialog.infohash.ui b/deluge/ui/gtk3/glade/add_torrent_dialog.infohash.ui new file mode 100644 index 0000000..8adbad3 --- /dev/null +++ b/deluge/ui/gtk3/glade/add_torrent_dialog.infohash.ui @@ -0,0 +1,217 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="dialog_infohash"> + <property name="width_request">462</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Add Infohash</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="decorated">False</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_magnet_add_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_magnet_add_ok"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image_dialog_magnet"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-revert-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">From Infohash</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Infohash:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_hash"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="activates_default">True</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Trackers:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTextView" id="text_trackers"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button_magnet_add_cancel</action-widget> + <action-widget response="-5">button_magnet_add_ok</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/add_torrent_dialog.ui b/deluge/ui/gtk3/glade/add_torrent_dialog.ui new file mode 100644 index 0000000..b0f507d --- /dev/null +++ b/deluge/ui/gtk3/glade/add_torrent_dialog.ui @@ -0,0 +1,1031 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="adjustment1"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment2"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment3"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment4"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkDialog" id="dialog_add_torrent"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Add Torrents</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_cancel_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add"> + <property name="label" translatable="yes">_Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkPaned" id="vpaned1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">3</property> + <property name="left_padding">5</property> + <property name="right_padding">5</property> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <property name="min_content_height">50</property> + <child> + <object class="GtkTreeView" id="listview_torrents"> + <property name="height_request">71</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection1"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">center</property> + <child> + <object class="GtkButton" id="button_file"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_file_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-open-symbolic</property> + <property name="icon_size">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_url"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_url_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">insert-link-symbolic</property> + <property name="icon_size">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_URL</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_hash"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_hash_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkImage" id="image5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-revert-symbolic</property> + <property name="icon_size">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Info_hash</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_remove"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_remove_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-remove-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Remove</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <property name="right_padding">5</property> + <child> + <object class="GtkNotebook" id="notebook1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_border">False</property> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkAlignment" id="alignment9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <child> + <object class="GtkBox" id="prefetch_hbox"> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkSpinner" id="prefetch_spinner"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="active">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="prefetch_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Please wait for files...</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">2</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="listview_files"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">1</property> + <property name="headers_visible">False</property> + <property name="enable_tree_lines">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection2"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkBox" id="hbox11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-open-symbolic</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Fi_les</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">3</property> + <property name="orientation">vertical</property> + <property name="spacing">3</property> + <child> + <object class="GtkFrame" id="frame7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="bottom_padding">1</property> + <property name="left_padding">5</property> + <property name="right_padding">5</property> + <child> + <object class="GtkBox" id="hbox_download_location_chooser"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label17"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Download Folder</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <property name="right_padding">5</property> + <child> + <object class="GtkBox" id="hbox_move_completed_chooser"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="chk_move_completed"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="margin_right">10</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_move_completed_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Move Complete Folder</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="separator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkFrame" id="frame6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">7</property> + <property name="right_padding">12</property> + <child> + <object class="GtkBox" id="vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_paused"> + <property name="label" translatable="yes">Add In _Paused State</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_prioritize"> + <property name="label" translatable="yes">Prioritize First/Last Pieces</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_sequential_download"> + <property name="label" translatable="yes">Sequential Download</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">When enabled, the piece picker will pick pieces in +sequence instead of rarest first. + +Enabling sequential download will affect the piece +distribution negatively in the swarm. It should be +used sparingly.</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_seed_mode"> + <property name="label" translatable="yes">Skip File Hash Check</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_markup">Useful if adding a complete torrent for seeding.</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_super_seeding"> + <property name="label" translatable="yes">Super Seeding</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_markup">Useful if adding a complete torrent for seeding.</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_pre_alloc"> + <property name="label" translatable="yes">Preallocate Disk Space</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Preallocate the disk space for the torrent files</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label_item"> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <property name="right_padding">5</property> + <child> + <object class="GtkGrid" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">12</property> + <child> + <object class="GtkSpinButton" id="spin_maxup"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment2</property> + <property name="update_policy">if-valid</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_maxconnections"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment3</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_maxupslots"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment4</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_maxdown"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment1</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Maximum torrent download speed</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Down Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Maximum torrent upload speed</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Up Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Maximum torrent connections</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Connections:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Maximum torrent upload slots</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Upload Slots:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Bandwidth</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="right_padding">5</property> + <child> + <object class="GtkButton" id="button_apply"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_apply_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="icon_name">emblem-ok-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Apply To All</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButton" id="button_revert"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_revert_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-revert-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Revert To Defaults</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkBox" id="hbox12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-properties-symbolic</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Options</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_cancel</action-widget> + <action-widget response="0">button_add</action-widget> + </action-widgets> + </object> + <object class="GtkListStore" id="liststore1"/> +</interface> diff --git a/deluge/ui/gtk3/glade/add_torrent_dialog.url.ui b/deluge/ui/gtk3/glade/add_torrent_dialog.url.ui new file mode 100644 index 0000000..6b75b23 --- /dev/null +++ b/deluge/ui/gtk3/glade/add_torrent_dialog.url.ui @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="url_dialog"> + <property name="width_request">462</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Add URL</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="decorated">False</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_add_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add_ok"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">From URL</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">URL:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_url"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="activates_default">True</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button_add_cancel</action-widget> + <action-widget response="-5">button_add_ok</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/connect_peer_dialog.ui b/deluge/ui/gtk3/glade/connect_peer_dialog.ui new file mode 100644 index 0000000..4a60751 --- /dev/null +++ b/deluge/ui/gtk3/glade/connect_peer_dialog.ui @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="connect_peer_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Add Peer</property> + <property name="window_position">mouse</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <property name="skip_pager_hint">True</property> + <property name="decorated">False</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button2"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button1"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Add Peer</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="txt_ip"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="activates_default">True</property> + <property name="width_chars">39</property> + <property name="text" translatable="yes">hostname:port</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button2</action-widget> + <action-widget response="1">button1</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/connection_manager.addhost.ui b/deluge/ui/gtk3/glade/connection_manager.addhost.ui new file mode 100644 index 0000000..ea5376e --- /dev/null +++ b/deluge/ui/gtk3/glade/connection_manager.addhost.ui @@ -0,0 +1,204 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.2 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="adjustment_port"> + <property name="upper">65535</property> + <property name="value">58846</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkDialog" id="addhost_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Add Host</property> + <property name="modal">True</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child type="titlebar"> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">15</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_addhost_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_addhost_add"> + <property name="label" translatable="yes">_Save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">5</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Hostname:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_hostname"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="invisible_char">•</property> + <property name="activates_default">True</property> + <property name="truncate_multiline">True</property> + <signal name="paste-clipboard" handler="on_entry_host_paste_clipboard" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Port:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spinbutton_port"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="halign">start</property> + <property name="max_length">5</property> + <property name="invisible_char">•</property> + <property name="width_chars">5</property> + <property name="max_width_chars">5</property> + <property name="progress_pulse_step">1</property> + <property name="adjustment">adjustment_port</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Username:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_username"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="invisible_char">•</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Password:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_password"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="visibility">False</property> + <property name="invisible_char">•</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_addhost_cancel</action-widget> + <action-widget response="1">button_addhost_add</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/connection_manager.ui b/deluge/ui/gtk3/glade/connection_manager.ui new file mode 100644 index 0000000..44f4b34 --- /dev/null +++ b/deluge/ui/gtk3/glade/connection_manager.ui @@ -0,0 +1,394 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkListStore" id="liststore_hostlist"> + <columns> + <!-- column-name host_id --> + <column type="gchararray"/> + <!-- column-name hostname --> + <column type="gchararray"/> + <!-- column-name port --> + <column type="gint"/> + <!-- column-name username --> + <column type="gchararray"/> + <!-- column-name password --> + <column type="gchararray"/> + <!-- column-name status --> + <column type="gchararray"/> + <!-- column-name version --> + <column type="gchararray"/> + <!-- column-name status_i18n --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkDialog" id="connection_manager"> + <property name="can_focus">False</property> + <property name="has_focus">True</property> + <property name="is_focus">True</property> + <property name="title" translatable="yes">Connection Manager</property> + <property name="modal">True</property> + <property name="window_position">center-on-parent</property> + <property name="default_width">300</property> + <property name="default_height">285</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_close"> + <property name="label" translatable="yes">_Close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_close_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_connect"> + <property name="label" translatable="yes">C_onnect</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_connect_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <property name="right_padding">5</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTreeView" id="treeview_hostlist"> + <property name="height_request">80</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">liststore_hostlist</property> + <signal name="row-activated" handler="on_hostlist_row_activated" swapped="no"/> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection"/> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">3</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButtonBox" id="hbuttonbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">True</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="button_addhost"> + <property name="label" translatable="yes">_Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_addhost_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_edithost"> + <property name="label" translatable="yes">_Edit</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_edithost_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_removehost"> + <property name="label" translatable="yes">_Remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_removehost_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_refresh"> + <property name="label" translatable="yes">_Refresh</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_refresh_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButton" id="button_startdaemon"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_startdaemon_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <child> + <object class="GtkImage" id="image_startdaemon"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">system-run-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">3</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_startdaemon"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Start Daemon</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Deluge Daemons</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkExpander" id="expander1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="expanded">True</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <property name="right_padding">5</property> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_autoconnect"> + <property name="label" translatable="yes">Auto-connect to selected Daemon</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_autostart"> + <property name="label" translatable="yes">Auto-start localhost daemon (if required)</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_donotshow"> + <property name="label" translatable="yes">Hide this dialog</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Startup Options</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_close</action-widget> + <action-widget response="0">button_connect</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/create_torrent_dialog.progress.ui b/deluge/ui/gtk3/glade/create_torrent_dialog.progress.ui new file mode 100644 index 0000000..e46ef17 --- /dev/null +++ b/deluge/ui/gtk3/glade/create_torrent_dialog.progress.ui @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="progress_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Creating Torrent</property> + <property name="window_position">center-on-parent</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="width_request">200</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/create_torrent_dialog.remote_path.ui b/deluge/ui/gtk3/glade/create_torrent_dialog.remote_path.ui new file mode 100644 index 0000000..4328330 --- /dev/null +++ b/deluge/ui/gtk3/glade/create_torrent_dialog.remote_path.ui @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="remote_path_dialog"> + <property name="width_request">462</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Enter Remote Path</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="decorated">False</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_add_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add_ok"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">network-workgroup-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Remote Path</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Path:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_path"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="activates_default">True</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button_add_cancel</action-widget> + <action-widget response="-5">button_add_ok</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/create_torrent_dialog.remote_save.ui b/deluge/ui/gtk3/glade/create_torrent_dialog.remote_save.ui new file mode 100644 index 0000000..7123054 --- /dev/null +++ b/deluge/ui/gtk3/glade/create_torrent_dialog.remote_save.ui @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="remote_save_dialog"> + <property name="width_request">462</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Save .torrent as</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="decorated">False</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_add_cancel1"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add_ok1"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">network-workgroup-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Save .torrent file</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Path:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_save_path"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="activates_default">True</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button_add_cancel1</action-widget> + <action-widget response="-5">button_add_ok1</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/create_torrent_dialog.ui b/deluge/ui/gtk3/glade/create_torrent_dialog.ui new file mode 100644 index 0000000..0d15940 --- /dev/null +++ b/deluge/ui/gtk3/glade/create_torrent_dialog.ui @@ -0,0 +1,844 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkListStore" id="liststore1"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">32 KiB</col> + </row> + <row> + <col id="0" translatable="yes">64 KiB</col> + </row> + <row> + <col id="0" translatable="yes">128 KiB</col> + </row> + <row> + <col id="0" translatable="yes">256 KiB</col> + </row> + <row> + <col id="0" translatable="yes">512 KiB</col> + </row> + <row> + <col id="0" translatable="yes">1 MiB</col> + </row> + <row> + <col id="0" translatable="yes">2 MiB</col> + </row> + <row> + <col id="0" translatable="yes">4 MiB</col> + </row> + <row> + <col id="0" translatable="yes">8 MiB</col> + </row> + <row> + <col id="0" translatable="yes">16 MiB</col> + </row> + </data> + </object> + <object class="GtkWindow" id="create_torrent_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Create Torrent</property> + <property name="window_position">center-on-parent</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-new-symbolic</property> + <property name="icon_size">5</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Create Torrent</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkBox" id="vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="treeview_files"> + <property name="height_request">30</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection1"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="hbuttonbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">center</property> + <child> + <object class="GtkButton" id="button_file"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_file_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-new-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_folder"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_folder_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">folder-open-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Fol_der</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_remote_path"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_remote_path_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">network-workgroup-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Remote Path</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Files</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="notebook1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Author:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_author"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Comments:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_comments"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkBox" id="hbox11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">dialog-information-symbolic</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Info</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkBox" id="hbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="tracker_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection2"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="vbuttonbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">1</property> + <property name="layout_style">center</property> + <child> + <object class="GtkButton" id="button_up"> + <property name="label" translatable="yes">_Up</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_up_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add"> + <property name="label" translatable="yes">_Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_remove"> + <property name="label" translatable="yes">_Remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_remove_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_down"> + <property name="label" translatable="yes">_Down</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_down_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkBox" id="hbox12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">text-editor-symbolic</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Trackers</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <child> + <object class="GtkTextView" id="textview_webseeds"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkBox" id="hbox13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">network-workgroup-symbolic</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Webseeds</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkBox" id="vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Piece Size:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combo_piece_size"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">liststore1</property> + <property name="active">2</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_private_flag"> + <property name="label" translatable="yes">Set Private Flag</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_add_to_session"> + <property name="label" translatable="yes">Add this torrent to the session</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkBox" id="hbox14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">preferences-other-symbolic</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Options</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_cancel_clicked" swapped="no"/> + <accelerator key="Escape" signal="clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_save"> + <property name="label" translatable="yes">_Save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_save_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/edit_trackers.add.ui b/deluge/ui/gtk3/glade/edit_trackers.add.ui new file mode 100644 index 0000000..39d1978 --- /dev/null +++ b/deluge/ui/gtk3/glade/edit_trackers.add.ui @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="add_tracker_dialog"> + <property name="width_request">400</property> + <property name="height_request">200</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Add Tracker</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_add_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_add_cancel_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add_ok"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_add_ok_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Add Trackers</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Trackers:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTextView" id="textview_trackers"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="left_margin">1</property> + <property name="right_margin">1</property> + <property name="accepts_tab">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button_add_cancel</action-widget> + <action-widget response="-5">button_add_ok</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/edit_trackers.edit.ui b/deluge/ui/gtk3/glade/edit_trackers.edit.ui new file mode 100644 index 0000000..fc3e51b --- /dev/null +++ b/deluge/ui/gtk3/glade/edit_trackers.edit.ui @@ -0,0 +1,175 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="edit_tracker_entry"> + <property name="width_request">400</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Edit Tracker</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_add_cancel1"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_edit_cancel_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add_ok1"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_edit_ok_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">text-editor-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Edit Tracker</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Tracker:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_edit_tracker"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="activates_default">True</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_add_cancel1</action-widget> + <action-widget response="1">button_add_ok1</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/edit_trackers.ui b/deluge/ui/gtk3/glade/edit_trackers.ui new file mode 100644 index 0000000..9b77a9b --- /dev/null +++ b/deluge/ui/gtk3/glade/edit_trackers.ui @@ -0,0 +1,247 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="edit_trackers_dialog"> + <property name="width_request">400</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Edit Trackers</property> + <property name="window_position">center-on-parent</property> + <property name="default_width">400</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <signal name="configure-event" handler="on_edit_trackers_dialog_configure_event" swapped="no"/> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_ok"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">text-editor-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Edit Trackers</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="tracker_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="vbuttonbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">1</property> + <property name="layout_style">center</property> + <child> + <object class="GtkButton" id="button_up"> + <property name="label" translatable="yes">_Up</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_up_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add"> + <property name="label" translatable="yes">_Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_exit"> + <property name="label" translatable="yes">_Edit</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_edit_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_remove"> + <property name="label" translatable="yes">_Remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_remove_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_down"> + <property name="label" translatable="yes">_Down</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_down_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_cancel</action-widget> + <action-widget response="1">button_ok</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/filtertree_menu.ui b/deluge/ui/gtk3/glade/filtertree_menu.ui new file mode 100644 index 0000000..d2861e1 --- /dev/null +++ b/deluge/ui/gtk3/glade/filtertree_menu.ui @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkImage" id="image22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">edit-select-all-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image22"> + <property name="can_focus">False</property> + <property name="icon_name">media-playback-pause-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">media-playback-start-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkMenu" id="filtertree_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="select_all"> + <property name="label" translatable="yes">_Select All</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image22</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_select_all" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_pause"> + <property name="label" translatable="yes">_Pause All</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image22</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_pause_all" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_resume"> + <property name="label" translatable="yes">Resu_me All</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Resume selected torrents.</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image23</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_resume_all" swapped="no"/> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/main_window.new_release.ui b/deluge/ui/gtk3/glade/main_window.new_release.ui new file mode 100644 index 0000000..f9c7fd5 --- /dev/null +++ b/deluge/ui/gtk3/glade/main_window.new_release.ui @@ -0,0 +1,249 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="new_release_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">New Release</property> + <property name="window_position">center-on-parent</property> + <property name="icon_name">deluge</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_close_new_release"> + <property name="label" translatable="yes">_Close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_goto_downloads"> + <property name="label" translatable="yes">_Goto Website</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">10</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image_new_release"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">image-missing</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">New Release Available!</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <child> + <object class="GtkGrid" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">2</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="label_available_version"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_available_version_text"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Available Version:</property> + <attributes> + <attribute name="style" value="italic"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_client_version"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_server_version"> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_server_version_text"> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Server Version</property> + <attributes> + <attribute name="style" value="italic"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_client_version_text"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Current Version:</property> + <attributes> + <attribute name="style" value="italic"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <child> + <object class="GtkCheckButton" id="chk_do_not_show_new_release"> + <property name="label" translatable="yes">Do not show this dialog in the future</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_close_new_release</action-widget> + <action-widget response="0">button_goto_downloads</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/main_window.tabs.menu_file.ui b/deluge/ui/gtk3/glade/main_window.tabs.menu_file.ui new file mode 100644 index 0000000..dd5d66b --- /dev/null +++ b/deluge/ui/gtk3/glade/main_window.tabs.menu_file.ui @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">zoom-fit-best-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">action-unavailable-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-next-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-up-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="image5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-down-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="image6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-open-symbolic</property> + </object> + <object class="GtkImage" id="image7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">folder-open-symbolic</property> + </object> + <object class="GtkMenu" id="menu_file_tab"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="menuitem_open_file"> + <property name="label" translatable="yes">_Open File</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image6</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_open_file_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_show_file"> + <property name="label" translatable="yes">_Show Folder</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image7</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_show_file_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_expand_all"> + <property name="label" translatable="yes">_Expand All</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image1</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_expand_all_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem_priority_sep"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_skip"> + <property name="label" translatable="yes">_Skip</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image2</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_skip_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_low"> + <property name="label" translatable="yes">_Low</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image5</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_low_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_normal"> + <property name="label" translatable="yes">_Normal</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image3</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_normal_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_high"> + <property name="label" translatable="yes">_High</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image4</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_high_activate" swapped="no"/> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/main_window.tabs.menu_peer.ui b/deluge/ui/gtk3/glade/main_window.tabs.menu_peer.ui new file mode 100644 index 0000000..d35ef77 --- /dev/null +++ b/deluge/ui/gtk3/glade/main_window.tabs.menu_peer.ui @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkMenu" id="menu_peer_tab"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="add_peer_menuitem"> + <property name="label" translatable="yes">_Add Peer</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Add a peer by its IP</property> + <property name="use_underline">True</property> + <property name="image">image1</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_add_peer_activate" swapped="no"/> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/main_window.tabs.ui b/deluge/ui/gtk3/glade/main_window.tabs.ui new file mode 100644 index 0000000..7ecf618 --- /dev/null +++ b/deluge/ui/gtk3/glade/main_window.tabs.ui @@ -0,0 +1,1657 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.2 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="spin_max_connections_adjustment"> + <property name="lower">-1</property> + <property name="upper">999999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="on_spin_value_changed" swapped="no"/> + </object> + <object class="GtkAdjustment" id="spin_max_download_adjustment"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="on_spin_value_changed" swapped="no"/> + </object> + <object class="GtkAdjustment" id="spin_max_upload_adjustment"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="on_spin_value_changed" swapped="no"/> + </object> + <object class="GtkAdjustment" id="spin_max_upload_slots_adjustment"> + <property name="lower">-1</property> + <property name="upper">999999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="on_spin_value_changed" swapped="no"/> + </object> + <object class="GtkAdjustment" id="spin_stop_ratio_adjustment"> + <property name="upper">999999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="on_spin_value_changed" swapped="no"/> + </object> + <object class="GtkWindow" id="tabs"> + <property name="can_focus">False</property> + <child type="titlebar"> + <placeholder/> + </child> + <child> + <object class="GtkNotebook" id="dummy_nb_see_main_win_torrent_info"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="enable_popup">True</property> + <child> + <object class="GtkScrolledWindow" id="status_tab"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment43"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">2</property> + <property name="left_padding">10</property> + <property name="right_padding">10</property> + <child> + <object class="GtkBox" id="vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="status_progress_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pulse_step">0.10000000149</property> + <property name="show_text">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="table8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">5</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="summary_download_speed"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="width_chars">20</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_total_downloaded"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_upload_speed"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="width_chars">15</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_seed_rank"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_availability"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_share_ratio"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_peers"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="width_chars">10</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_eta"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">5</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_active_time"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">5</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_seed_time"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">5</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_last_transfer"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + <property name="wrap_mode">char</property> + </object> + <packing> + <property name="left_attach">5</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_last_seen_complete"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + <property name="wrap_mode">char</property> + </object> + <packing> + <property name="left_attach">5</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label42"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Down Speed:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label43"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Up Speed:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label38"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Downloaded:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label39"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Uploaded:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_seeds"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="width_chars">10</property> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_total_uploaded"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="width_chars">15</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_seeds"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Seeds:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_peers"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Peers:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label41"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Share Ratio:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_availablity"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Availability:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_seed_rank"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Seed Rank:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_eta"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">ETA Time:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_last_transfer"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Last Transfer:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_active_time"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Active Time:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_last_seen_complete"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Complete Seen:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_seed_time"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Seeding Time:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="status_tab_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Status</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="details_tab"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment54"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">2</property> + <property name="left_padding">10</property> + <property name="right_padding">15</property> + <child> + <object class="GtkGrid" id="table_details"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">5</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="summary_name"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_total_size"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_num_files"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_completed"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_date_added"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_pieces"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Pieces:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_pieces"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">4</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_hash"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="wrap">True</property> + <property name="selectable">True</property> + <property name="width_chars">40</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_torrent_path"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="wrap">True</property> + <property name="wrap_mode">char</property> + <property name="selectable">True</property> + <property name="ellipsize">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_comments"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="wrap_mode">char</property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">6</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_creator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="wrap_mode">char</property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_name"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Name:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_path"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Download Folder:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_date_added"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Added:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_total_size"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Total Size:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_num_files"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Total Files:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_hash"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Hash:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_creator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Created By:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_comments"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Comments:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">6</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_completed"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Completed:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="vseparator5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + <property name="height">3</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="details_tab_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Details</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="files_tab"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTreeView" id="files_listview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection1"/> + </child> + </object> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="files_tab_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Fi_les</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="peers_tab"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTreeView" id="peers_listview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection2"/> + </child> + </object> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="peers_tab_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Peers</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="position">3</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="options_tab"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">2</property> + <property name="left_padding">5</property> + <property name="right_padding">15</property> + <child> + <object class="GtkGrid" id="table6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkAlignment" id="alignment8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="left_padding">5</property> + <property name="right_padding">5</property> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="label_owner"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Owner:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_owner"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap_mode">char</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">10</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_shared"> + <property name="label" translatable="yes">Shared</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_prioritize_first_last"> + <property name="label" translatable="yes">Prioritize First/Last</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_sequential_download"> + <property name="label" translatable="yes">Sequential Download</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_super_seeding"> + <property name="label" translatable="yes">Super Seeding</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_move_completed"> + <property name="label" translatable="yes">Move completed:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_move_completed_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox_move_completed_path_chooser"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">6</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="vbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">15</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_auto_managed"> + <property name="label" translatable="yes">Auto Managed</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_stop_at_ratio"> + <property name="label" translatable="yes">Stop seed at ratio:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_stop_at_ratio_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <property name="right_padding">10</property> + <child> + <object class="GtkSpinButton" id="spin_stop_ratio"> + <property name="width_request">50</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">spin_stop_ratio_adjustment</property> + <property name="digits">1</property> + <property name="numeric">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkCheckButton" id="chk_remove_at_ratio"> + <property name="label" translatable="yes">Remove at ratio</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_toggled" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">7</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButton" id="button_apply"> + <property name="label" translatable="yes">_Apply</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_underline">True</property> + <signal name="clicked" handler="on_button_apply_clicked" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkGrid" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="row_spacing">2</property> + <property name="column_spacing">4</property> + <child> + <object class="GtkSpinButton" id="spin_max_connections"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">spin_max_connections_adjustment</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_upload"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">spin_max_upload_adjustment</property> + <property name="digits">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_download"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">spin_max_download_adjustment</property> + <property name="climb_rate">1</property> + <property name="digits">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Upload Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">K/s</property> + <property name="ellipsize">start</property> + <attributes> + <attribute name="scale" value="0.90000000000000002"/> + </attributes> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">K/s</property> + <attributes> + <attribute name="scale" value="0.90000000000000002"/> + </attributes> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_upload_slots"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">spin_max_upload_slots_adjustment</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Download Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Connections:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Upload Slots:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Bandwidth Limits</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">4</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="options_tab_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Options</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="position">4</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="trackers_tab"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="bottom_padding">2</property> + <property name="left_padding">10</property> + <property name="right_padding">15</property> + <child> + <object class="GtkGrid" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">5</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="label_tracker"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Current Tracker:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_tracker"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_next_announce"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_tracker_status"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + <property name="wrap_mode">char</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_tracker_total"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="summary_private"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap_mode">char</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_tracker_total"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Total Trackers:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_tracker_status"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Tracker Status:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_next_announce"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Next Announce:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_private"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Private Torrent:</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_edit_trackers"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="top_padding">5</property> + <child> + <object class="GtkButton" id="button_edit_trackers"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_edit_trackers_clicked" swapped="no"/> + <child> + <object class="GtkLabel" id="label_edit_trackers"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Edit Trackers</property> + <property name="use_underline">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">5</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="trackers_tab_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Trackers</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="position">5</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/main_window.ui b/deluge/ui/gtk3/glade/main_window.ui new file mode 100644 index 0000000..e726756 --- /dev/null +++ b/deluge/ui/gtk3/glade/main_window.ui @@ -0,0 +1,797 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.2 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkImage" id="about-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">help-about-symbolic</property> + </object> + <object class="GtkImage" id="add-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkAdjustment" id="adjustment1"> + <property name="lower">-1</property> + <property name="upper">999999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment2"> + <property name="lower">-1</property> + <property name="upper">99999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment3"> + <property name="lower">-1</property> + <property name="upper">999999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment4"> + <property name="lower">-1</property> + <property name="upper">999999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment5"> + <property name="upper">99999</property> + <property name="value">2</property> + <property name="step_increment">0.10000000000000001</property> + <property name="page_increment">10</property> + </object> + <object class="GtkImage" id="connection-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">preferences-system-network-symbolic</property> + </object> + <object class="GtkImage" id="image4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-up-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="image5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-top-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="image6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">zoom-fit-best-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkAccelGroup" id="main_accelgroup"/> + <object class="GtkImage" id="new-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-new-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="prefs-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">preferences-system-symbolic</property> + </object> + <object class="GtkImage" id="quit-daemon-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">system-shutdown-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="quit_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">application-exit-symbolic</property> + </object> + <object class="GtkWindow" id="main_window"> + <property name="can_focus">False</property> + <property name="title">Deluge</property> + <accel-groups> + <group name="main_accelgroup"/> + </accel-groups> + <child type="titlebar"> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkMenuBar" id="menubar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="menu_file"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="submenu_file"> + <property name="can_focus">False</property> + <property name="accel_group">main_accelgroup</property> + <property name="accel_path"><Deluge-MainWindow>/File</property> + <child> + <object class="GtkImageMenuItem" id="menuitem_addtorrent"> + <property name="label" translatable="yes">_Add Torrent</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">add-image</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_addtorrent_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_createtorrent"> + <property name="label" translatable="yes">_Create Torrent</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">new-image</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_createtorrent_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem"> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_quitdaemon"> + <property name="label" translatable="yes">Quit & _Shutdown Daemon</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">quit-daemon-image</property> + <property name="use_stock">False</property> + <property name="always_show_image">True</property> + <signal name="activate" handler="on_menuitem_quitdaemon_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_quit"> + <property name="label" translatable="yes">_Quit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">quit_image</property> + <property name="use_stock">False</property> + <property name="accel_group">main_accelgroup</property> + <signal name="activate" handler="on_menuitem_quit_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_edit"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="submenu_edit"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="accel_group">main_accelgroup</property> + <property name="accel_path"><Deluge-MainWindow>/Edit</property> + <child> + <object class="GtkImageMenuItem" id="menuitem_preferences"> + <property name="label" translatable="yes">_Preferences</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">prefs-image</property> + <property name="use_stock">False</property> + <property name="accel_group">main_accelgroup</property> + <signal name="activate" handler="on_menuitem_preferences_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_connectionmanager"> + <property name="label" translatable="yes">_Connection Manager</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">connection-image</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_connectionmanager_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_torrent"> + <property name="can_focus">False</property> + <property name="accel_path"><MainWindow>/Torrent</property> + <property name="label" translatable="yes">_Torrent</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_view"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_View</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="submenu_view"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="accel_group">main_accelgroup</property> + <property name="accel_path"><Deluge-MainWindow>/View</property> + <child> + <object class="GtkCheckMenuItem" id="menuitem_toolbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Toolbar</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <signal name="toggled" handler="on_menuitem_toolbar_toggled" swapped="no"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="menuitem_sidebar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Sidebar</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <signal name="toggled" handler="on_menuitem_sidebar_toggled" swapped="no"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="menuitem_statusbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Status_bar</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <signal name="toggled" handler="on_menuitem_statusbar_toggled" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_tabs"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">T_abs</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_columns"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Columns</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="find_menuitem"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Find ...</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_search_filter_toggle" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_Sidebar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="label" translatable="yes">S_idebar</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="menu3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <child> + <object class="GtkCheckMenuItem" id="sidebar_show_zero"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Show _Zero Hits</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <signal name="toggled" handler="on_menuitem_sidebar_zero_toggled" swapped="no"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="sidebar_show_trackers"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Show _Trackers</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <signal name="toggled" handler="on_menuitem_sidebar_trackers_toggled" swapped="no"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="sidebar_show_owners"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Show _Owners</property> + <property name="active">True</property> + <signal name="toggled" handler="on_menuitem_sidebar_owners_toggled" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menu_help"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Help</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="submenu_help"> + <property name="can_focus">False</property> + <property name="accel_group">main_accelgroup</property> + <property name="accel_path"><Deluge-MainWindow>/Help</property> + <child> + <object class="GtkImageMenuItem" id="menuitem_homepage"> + <property name="label" translatable="yes">_Homepage</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_homepage_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_faq"> + <property name="label" translatable="yes">_FAQ</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Frequently Asked Questions</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_faq_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_community"> + <property name="label" translatable="yes">_Community</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <property name="always_show_image">True</property> + <signal name="activate" handler="on_menuitem_community_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem56"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_about"> + <property name="label" translatable="yes">_About</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">about-image</property> + <property name="use_stock">False</property> + <property name="accel_group">main_accelgroup</property> + <signal name="activate" handler="on_menuitem_about_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToolbar" id="toolbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_size">2</property> + <child> + <object class="GtkToolButton" id="toolbutton_add"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Add torrent</property> + <property name="label" translatable="yes">Add Torrent</property> + <property name="icon_name">list-add-symbolic</property> + <signal name="clicked" handler="on_toolbutton_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_remove"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Remove torrent</property> + <property name="label" translatable="yes">Remove Torrent</property> + <property name="icon_name">list-remove-symbolic</property> + <signal name="clicked" handler="on_toolbutton_remove_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_filter"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Filter torrents by name. +This will filter torrents for the current selection on the sidebar.</property> + <property name="label" translatable="yes">Filter</property> + <property name="icon_name">system-search-symbolic</property> + <signal name="clicked" handler="on_search_filter_toggle" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separatortoolitem1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_pause"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Pause the selected torrents</property> + <property name="label" translatable="yes">Pause</property> + <property name="icon_name">media-playback-pause-symbolic</property> + <signal name="clicked" handler="on_toolbutton_pause_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_resume"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Resume the selected torrents</property> + <property name="label" translatable="yes">Resume</property> + <property name="icon_name">media-playback-start-symbolic</property> + <signal name="clicked" handler="on_toolbutton_resume_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separatortoolitem2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_queue_up"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Queue Torrent Up</property> + <property name="label" translatable="yes">Queue Up</property> + <property name="icon_name">go-up-symbolic</property> + <signal name="clicked" handler="on_toolbutton_queue_up_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_queue_down"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Queue Torrent Down</property> + <property name="label" translatable="yes">Queue Down</property> + <property name="icon_name">go-down-symbolic</property> + <signal name="clicked" handler="on_toolbutton_queue_down_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="toolbutton1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_preferences"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Preferences</property> + <property name="label" translatable="yes">Preferences</property> + <property name="icon_name">preferences-system-symbolic</property> + <signal name="clicked" handler="on_toolbutton_preferences_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="toolbutton_connectionmanager"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Connection Manager</property> + <property name="label" translatable="yes">Connection Manager</property> + <property name="icon_name">preferences-system-network-symbolic</property> + <signal name="clicked" handler="on_toolbutton_connectionmanager_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <child> + <object class="GtkPaned" id="tabsbar_pane"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkPaned" id="sidebar_pane"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkNotebook" id="sidebar_notebook"> + <property name="can_focus">True</property> + <property name="scrollable">True</property> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">True</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="search_box"> + <property name="can_focus">False</property> + <child> + <object class="GtkButton" id="close_search_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Close</property> + <property name="relief">none</property> + <signal name="clicked" handler="on_close_search_button_clicked" swapped="no"/> + <child> + <object class="GtkImage" id="close_search_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">window-close-symbolic</property> + <property name="icon_size">2</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Filter:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">1</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="search_torrents_entry"> + <property name="width_request">350</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">Filter torrents by name. +This will filter torrents for the current selection on the sidebar.</property> + <property name="invisible_char">•</property> + <property name="truncate_multiline">True</property> + <property name="caps_lock_warning">False</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <property name="primary_icon_sensitive">False</property> + <property name="secondary_icon_tooltip_text" translatable="yes">Clear the search</property> + <property name="secondary_icon_tooltip_markup" translatable="yes">Clear the search</property> + <signal name="changed" handler="on_search_torrents_entry_changed" swapped="no"/> + <signal name="icon-press" handler="on_search_torrents_entry_icon_press" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">1</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="search_torrents_match"> + <property name="label" translatable="yes">_Match Case</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_search_torrents_match_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">1</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">out</property> + <child> + <object class="GtkTreeView" id="torrent_view"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="rules_hint">True</property> + <property name="enable_search">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="torrent_info"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tab_pos">left</property> + <property name="show_border">False</property> + <property name="scrollable">True</property> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">False</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkStatusbar" id="statusbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/move_storage_dialog.ui b/deluge/ui/gtk3/glade/move_storage_dialog.ui new file mode 100644 index 0000000..542d40a --- /dev/null +++ b/deluge/ui/gtk3/glade/move_storage_dialog.ui @@ -0,0 +1,163 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="move_storage_dialog"> + <property name="width_request">500</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Move Download Folder</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_ok"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-save-as-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Move the torrent(s) download folder.</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox_entry"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Destination:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button_cancel</action-widget> + <action-widget response="-5">button_ok</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/other_dialog.ui b/deluge/ui/gtk3/glade/other_dialog.ui new file mode 100644 index 0000000..01a5597 --- /dev/null +++ b/deluge/ui/gtk3/glade/other_dialog.ui @@ -0,0 +1,188 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="adjustment1"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="value">-1</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkDialog" id="other_dialog"> + <property name="app_paintable">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="modal">True</property> + <property name="window_position">mouse</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <property name="skip_pager_hint">True</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button3"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button4"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkImage" id="image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_header"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label">label</property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator" id="hseparator2"> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_length">6</property> + <property name="activates_default">True</property> + <property name="width_chars">6</property> + <property name="adjustment">adjustment1</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_type"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">label</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button3</action-widget> + <action-widget response="-5">button4</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/path_combo_chooser.ui b/deluge/ui/gtk3/glade/path_combo_chooser.ui new file mode 100644 index 0000000..871bac0 --- /dev/null +++ b/deluge/ui/gtk3/glade/path_combo_chooser.ui @@ -0,0 +1,1001 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="adjustment1"> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment2"> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment3"> + <property name="lower">-1</property> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkDialog" id="completion_config_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Properties</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <signal name="delete-event" handler="on_completion_config_dialog_delete_event" swapped="no"/> + <signal name="key-release-event" handler="on_completion_config_dialog_key_release_event" swapped="no"/> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="config_dialog_button_close"> + <property name="label" translatable="yes">Close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_config_dialog_button_close_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="config_general_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">3</property> + <property name="left_padding">15</property> + <property name="right_padding">10</property> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="visible_rows_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Max drop down rows</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="visible_rows_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_length">2</property> + <property name="adjustment">adjustment3</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + <signal name="value-changed" handler="on_visible_rows_spinbutton_value_changed" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>General</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">3</property> + <property name="left_padding">12</property> + <property name="right_padding">5</property> + <child> + <object class="GtkBox" id="vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="show_path_entry_checkbutton"> + <property name="label" translatable="yes">Show path entry</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_path_entry_checkbutton_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="show_filechooser_checkbutton"> + <property name="label" translatable="yes">Show file chooser</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_filechooser_checkbutton_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkCheckButton" id="show_folder_name_on_button_checkbutton"> + <property name="label" translatable="yes">Show folder name</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_folder_name_on_button_checkbutton_toggled" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Path Chooser Type</property> + <property name="use_markup">True</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">3</property> + <property name="left_padding">12</property> + <property name="right_padding">5</property> + <child> + <object class="GtkGrid" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">5</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkCheckButton" id="enable_auto_completion_checkbutton"> + <property name="label" translatable="yes">Enable autocomplete</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_enable_auto_completion_checkbutton_toggled" swapped="no"/> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="show_hidden_files_checkbutton"> + <property name="label" translatable="yes">Show hidden files</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_hidden_files_checkbutton_toggled" swapped="no"/> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="set_completion_accelerator_button"> + <property name="label" translatable="yes">Set new key</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Press this key to set new key accelerators to trigger auto-complete</property> + <signal name="activate" handler="on_set_completion_accelerator_button_pressed" swapped="no"/> + <signal name="clicked" handler="on_set_completion_accelerator_button_clicked" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Autocomplete</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="config_short_cuts_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">etched-out</property> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">4</property> + <property name="bottom_padding">5</property> + <property name="left_padding">15</property> + <property name="right_padding">5</property> + <child> + <object class="GtkGrid" id="table3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">2</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Autocomplete</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Save path</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Ctrl+S</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="completion_accelerator_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Ctrl+E</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Ctrl+R</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Ctrl+H</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Ctrl+D</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Edit path</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Remove path</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="margin_right">8</property> + <property name="label" translatable="yes">Toggle hidden files</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Default path</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Shortcuts</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">config_dialog_button_close</action-widget> + </action-widgets> + </object> + <object class="GtkWindow" id="combobox_window"> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="entry_combobox_hbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">3</property> + <signal name="realize" handler="on_entry_combobox_hbox_realize" swapped="no"/> + <child> + <object class="GtkFileChooserButton" id="filechooser_button"> + <property name="width_request">160</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="action">select-folder</property> + <property name="local_only">False</property> + <property name="preview_widget_active">False</property> + <property name="title" translatable="yes">Select a Directory</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_open_dialog"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_open_dialog_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">folder-open-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="folder_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_text"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="invisible_char">•</property> + <signal name="changed" handler="on_entry_text_changed" swapped="no"/> + <signal name="delete-text" handler="on_entry_text_delete_text" swapped="no"/> + <signal name="focus-out-event" handler="on_entry_text_focus_out_event" swapped="no"/> + <signal name="insert-text" handler="on_entry_text_insert_text" swapped="yes"/> + <signal name="key-press-event" handler="on_entry_text_key_press_event" swapped="no"/> + <signal name="scroll-event" handler="on_entry_text_scroll_event" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="button_toggle_dropdown"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="focus_on_click">False</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Saved paths</property> + <signal name="button-press-event" handler="on_button_toggle_dropdown_button_press_event" swapped="no"/> + <signal name="scroll-event" handler="on_button_toggle_dropdown_scroll_event" swapped="no"/> + <signal name="toggled" handler="on_button_toggle_dropdown_toggled" swapped="no"/> + <child> + <object class="GtkBox" id="vbox1"> + <property name="height_request">15</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkArrow" id="arrow2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="arrow_type">down</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkListStore" id="completion_tree_store"> + <columns> + <!-- column-name text --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkWindow" id="completion_popup_window"> + <property name="can_focus">False</property> + <property name="type">popup</property> + <property name="resizable">False</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">combo</property> + <property name="skip_taskbar_hint">True</property> + <property name="skip_pager_hint">True</property> + <property name="decorated">False</property> + <property name="deletable">False</property> + <signal name="button-press-event" handler="on_completion_popup_window_button_press_event" swapped="no"/> + <signal name="focus-out-event" handler="on_completion_popup_window_focus_out_event" swapped="no"/> + <signal name="key-press-event" handler="on_completion_popup_window_key_press_event" swapped="no"/> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="popup_content_box1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">3</property> + <property name="spacing">1</property> + <child> + <object class="GtkScrolledWindow" id="completion_scrolled_window"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="completion_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">completion_tree_store</property> + <property name="headers_visible">False</property> + <property name="headers_clickable">False</property> + <property name="enable_search">False</property> + <property name="search_column">0</property> + <property name="show_expanders">False</property> + <signal name="button-press-event" handler="on_completion_treeview_mouse_button_press_event" swapped="no"/> + <signal name="key-press-event" handler="on_completion_treeview_key_press_event" swapped="no"/> + <signal name="motion-notify-event" handler="on_completion_treeview_motion_notify_event" swapped="no"/> + <signal name="scroll-event" handler="on_completion_treeview_scroll_event" swapped="no"/> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection1"/> + </child> + <child> + <object class="GtkTreeViewColumn" id="completion_treeview_column"> + <property name="sizing">autosize</property> + <property name="fixed_width">129</property> + <property name="title" translatable="yes">column</property> + <property name="expand">True</property> + <child> + <object class="GtkCellRendererText" id="completion_cellrenderertext"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkDialog" id="filechooserdialog"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Choose a folder</property> + <property name="modal">True</property> + <property name="window_position">center</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="box1"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="buttonbox1"> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="filechooser_button_cancel"> + <property name="label" translatable="yes">Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="filechooser_button_open"> + <property name="label" translatable="yes">Open</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFileChooserWidget" id="filechooser_widget"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="action">select-folder</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="2">filechooser_button_cancel</action-widget> + <action-widget response="0">filechooser_button_open</action-widget> + </action-widgets> + </object> + <object class="GtkListStore" id="stored_values_tree_store"> + <columns> + <!-- column-name text --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkWindow" id="stored_values_popup_window"> + <property name="can_focus">False</property> + <property name="type">popup</property> + <property name="resizable">False</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">popup-menu</property> + <property name="skip_taskbar_hint">True</property> + <property name="skip_pager_hint">True</property> + <property name="decorated">False</property> + <property name="deletable">False</property> + <signal name="button-press-event" handler="on_stored_values_popup_window_button_press_event" swapped="no"/> + <signal name="focus-out-event" handler="on_stored_values_popup_window_focus_out_event" swapped="no"/> + <signal name="hide" handler="on_stored_values_popup_window_hide" swapped="no"/> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">3</property> + <property name="spacing">2</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="hscrollbar_policy">never</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="stored_values_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">stored_values_tree_store</property> + <property name="headers_visible">False</property> + <property name="headers_clickable">False</property> + <property name="search_column">0</property> + <property name="show_expanders">False</property> + <signal name="button-press-event" handler="on_stored_values_treeview_mouse_button_press_event" swapped="no"/> + <signal name="key-press-event" handler="on_stored_values_treeview_key_press_event" swapped="no"/> + <signal name="key-release-event" handler="on_stored_values_treeview_key_release_event" swapped="no"/> + <signal name="scroll-event" handler="on_stored_values_treeview_scroll_event" swapped="no"/> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection2"/> + </child> + <child> + <object class="GtkTreeViewColumn" id="stored_values_treeview_column"> + <property name="sizing">autosize</property> + <property name="fixed_width">127</property> + <property name="title" translatable="yes">column</property> + <property name="expand">True</property> + <child> + <object class="GtkCellRendererText" id="stored_values_cellrenderertext"> + <signal name="edited" handler="on_cellrenderertext_edited" swapped="no"/> + </object> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkButtonBox" id="buttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">1</property> + <property name="layout_style">start</property> + <signal name="key-press-event" handler="on_buttonbox_key_press_event" swapped="no"/> + <child> + <object class="GtkButton" id="button_add"> + <property name="label" translatable="yes">Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Add the current entry value to the list</property> + <signal name="clicked" handler="on_button_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_edit"> + <property name="label" translatable="yes">Edit</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Edit the selected entry</property> + <signal name="clicked" handler="on_button_edit_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_remove"> + <property name="label" translatable="yes">Remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Remove the selected entry</property> + <signal name="clicked" handler="on_button_remove_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_up"> + <property name="label" translatable="yes">Up</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Move the selected entry up</property> + <signal name="clicked" handler="on_button_up_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_down"> + <property name="label" translatable="yes">Down</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Move the selected entry down</property> + <signal name="clicked" handler="on_button_down_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_default"> + <property name="label" translatable="yes">Default</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="tooltip_text" translatable="yes">No default path set</property> + <signal name="clicked" handler="on_button_default_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_properties"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Open properties dialog</property> + <signal name="clicked" handler="on_button_properties_clicked" swapped="no"/> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-properties-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">6</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">2</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/preferences_dialog.ui b/deluge/ui/gtk3/glade/preferences_dialog.ui new file mode 100644 index 0000000..720dc6b --- /dev/null +++ b/deluge/ui/gtk3/glade/preferences_dialog.ui @@ -0,0 +1,5014 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.2 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="adjustment_cache_expiry"> + <property name="lower">1</property> + <property name="upper">32000</property> + <property name="value">60</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_cache_size"> + <property name="upper">999999</property> + <property name="value">100</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_share_ratio"> + <property name="lower">0.5</property> + <property name="upper">100</property> + <property name="value">2</property> + <property name="step_increment">0.10000000000000001</property> + <property name="page_increment">1</property> + </object> + <object class="GtkAdjustment" id="adjustment_share_ratio_limit"> + <property name="lower">-1</property> + <property name="upper">100</property> + <property name="value">1.5</property> + <property name="step_increment">0.10000000000000001</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_active"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_daemon_port"> + <property name="upper">65535</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_downloading"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_incoming_port"> + <property name="upper">65535</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_conn_global"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_conn_per_sec"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_conn_per_torrent"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_download"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_download_per_torrent"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_half_open_conn"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_upload"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_upload_per_torrent"> + <property name="lower">-1</property> + <property name="upper">2097151</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_upload_slots_global"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_max_upload_slots_per_torrent"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_outgoing_port_max"> + <property name="upper">65535</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_outgoing_port_min"> + <property name="upper">65535</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_proxy_port"> + <property name="upper">65535</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_seed_time_limit"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_spin_seeding"> + <property name="lower">-1</property> + <property name="upper">9999</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="adjustment_time_ratio_limit"> + <property name="lower">-1</property> + <property name="upper">100</property> + <property name="value">6</property> + <property name="step_increment">0.10000000000000001</property> + <property name="page_increment">10</property> + </object> + <object class="GtkListStore" id="liststore1"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">Forced</col> + </row> + <row> + <col id="0" translatable="yes">Enabled</col> + </row> + <row> + <col id="0" translatable="yes">Disabled</col> + </row> + </data> + </object> + <object class="GtkListStore" id="liststore2"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">Handshake</col> + </row> + <row> + <col id="0" translatable="yes">Full Stream</col> + </row> + <row> + <col id="0" translatable="yes">Either</col> + </row> + </data> + </object> + <object class="GtkListStore" id="liststore3"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">Forced</col> + </row> + <row> + <col id="0" translatable="yes">Enabled</col> + </row> + <row> + <col id="0" translatable="yes">Disabled</col> + </row> + </data> + </object> + <object class="GtkListStore" id="liststore8"> + <columns> + <!-- column-name language_code --> + <column type="gchararray"/> + <!-- column-name language_name --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkListStore" id="liststore_proxy"> + <columns> + <!-- column-name item --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0" translatable="yes">None</col> + </row> + <row> + <col id="0" translatable="yes">Socks4</col> + </row> + <row> + <col id="0" translatable="yes">Socks5</col> + </row> + <row> + <col id="0" translatable="yes">Socks5 Auth</col> + </row> + <row> + <col id="0" translatable="yes">HTTP</col> + </row> + <row> + <col id="0" translatable="yes">HTTP Auth</col> + </row> + <row> + <col id="0" translatable="yes">I2P</col> + </row> + </data> + </object> + <object class="GtkDialog" id="pref_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Preferences</property> + <property name="modal">True</property> + <property name="window_position">center-on-parent</property> + <property name="default_width">450</property> + <property name="default_height">500</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <signal name="configure-event" handler="on_pref_dialog_configure_event" swapped="no"/> + <signal name="delete-event" handler="on_pref_dialog_delete_event" swapped="no"/> + <child type="titlebar"> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_cancel"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_cancel_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_apply"> + <property name="label" translatable="yes">_Apply</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_apply_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_ok"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_ok_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkPaned" id="hpaned1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <child> + <object class="GtkTreeView" id="treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection1"/> + </child> + </object> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">True</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="notebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_tabs">False</property> + <property name="scrollable">True</property> + <child> + <object class="GtkScrolledWindow" id="InterfaceScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="interface_viewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="interface_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame_app_mode"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment_app_mode"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">3</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="hbox_app_mode"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkRadioButton" id="radio_standalone"> + <property name="label" translatable="yes">Standalone</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">The standalone self-contained application</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radio_thinclient"> + <property name="label" translatable="yes">Thin Client</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Connect to a Deluge daemon (deluged)</property> + <property name="draw_indicator">True</property> + <property name="group">radio_standalone</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">7</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label_client_mode"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Application Mode</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame29"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment47"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="vbox27"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_show_rate_in_title"> + <property name="label" translatable="yes">Show session speed in titlebar</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_focus_main_window_on_add"> + <property name="label" translatable="yes">Focus window when adding torrent</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="urldetect_toggle"> + <property name="label" translatable="yes">Detect torrent URLs from clipboard</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Automatically open Add Torrent dialog when clipboard contains a torrent URL</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_urldetect_toggle_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="piecesbar_toggle"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">The pieces bar +will increase bandwidth use between client +and daemon (does not apply in Standalone mode).</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_piecesbar_toggle_toggled" swapped="no"/> + <child> + <object class="GtkLabel" id="label62"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Show a pieces bar in Status tab</property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkExpander" id="piecebar_colors_expander"> + <property name="can_focus">True</property> + <child> + <object class="GtkAlignment" id="alignment58"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">25</property> + <child> + <object class="GtkGrid" id="table6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">1</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label66"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Completed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="completed_color"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="color-set" handler="on_completed_color_set" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label67"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Downloading:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="downloading_color"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="color-set" handler="on_downloading_color_set" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label69"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Waiting:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="waiting_color"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="color-set" handler="on_waiting_color_set" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label70"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Missing:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="missing_color"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="color-set" handler="on_missing_color_set" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkButton" id="revert_color_completed"> + <property name="label" translatable="yes">_Revert</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="tooltip_text" translatable="yes">Revert color to default</property> + <property name="use_underline">True</property> + <property name="image_position">right</property> + <signal name="clicked" handler="on_revert_color_completed_clicked" swapped="no"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="revert_color_downloading"> + <property name="label" translatable="yes">_Revert</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="tooltip_text" translatable="yes">Revert color to default</property> + <property name="use_underline">True</property> + <property name="image_position">right</property> + <signal name="clicked" handler="on_revert_color_downloading_clicked" swapped="no"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="revert_color_waiting"> + <property name="label" translatable="yes">_Revert</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="tooltip_text" translatable="yes">Revert color to default</property> + <property name="use_underline">True</property> + <property name="image_position">right</property> + <signal name="clicked" handler="on_revert_color_waiting_clicked" swapped="no"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="revert_color_missing"> + <property name="label" translatable="yes">_Revert</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="tooltip_text" translatable="yes">Revert color to default</property> + <property name="use_underline">True</property> + <property name="image_position">right</property> + <signal name="clicked" handler="on_revert_color_missing_clicked" swapped="no"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label73"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Piece Colors</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label106"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Main Window</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="vbox17"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_use_tray"> + <property name="label" translatable="yes">Enable system tray icon</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_tray_type"> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="hbox_tray_type"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkRadioButton" id="radio_appind"> + <property name="label" translatable="yes">App Indicator</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <property name="group">radio_systray</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">10</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radio_systray"> + <property name="label" translatable="yes">Systray</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_min_on_close"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkCheckButton" id="chk_min_on_close"> + <property name="label" translatable="yes">Minimize to tray on close</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_start_in_tray"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkCheckButton" id="chk_start_in_tray"> + <property name="label" translatable="yes">Start in tray</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_lock_tray"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="bottom_padding">3</property> + <property name="left_padding">10</property> + <child> + <object class="GtkCheckButton" id="chk_lock_tray"> + <property name="label" translatable="yes">Password protect system tray</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_tray_password"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">20</property> + <child> + <object class="GtkBox" id="hbox_tray_password"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="password_label"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Password:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="txt_tray_password"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="visibility">False</property> + <property name="width_chars">16</property> + <property name="text">********</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label52"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">System Tray</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame_theme"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label-xalign">0</property> + <property name="shadow-type">none</property> + <child> + <object class="GtkAlignment" id="alignment_theme"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="top-padding">3</property> + <property name="left-padding">10</property> + <child> + <object class="GtkCheckButton" id="chk_prefer_dark_theme"> + <property name="label" translatable="yes">Prefer Dark</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip_text" translatable="yes">If available, use dark variant of GTK theme.</property> + <property name="draw-indicator">True</property> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label_theme"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Theme</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment35"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="vbox18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkAlignment" id="alignment36"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="chk_show_new_releases"> + <property name="label" translatable="yes">Notify about new releases</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label43"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Updates</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="checkbutton_language"> + <property name="label" translatable="yes">System Default</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_checkbutton_language_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">20</property> + <child> + <object class="GtkComboBox" id="combobox_language"> + <property name="can_focus">False</property> + <property name="model">liststore8</property> + <property name="active">0</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext8"/> + <attributes> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Language</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="label29"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 6</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="DownloadsScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="downloads_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkGrid" id="table9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">1</property> + <child> + <object class="GtkCheckButton" id="chk_move_completed"> + <property name="label" translatable="yes">Move completed to:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_copy_torrent_file"> + <property name="label" translatable="yes">Copy of .torrent files to:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_del_copy_torrent_file"> + <property name="label" translatable="yes">Delete copy of torrent file on remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Delete the copy of the torrent file created when the torrent is removed</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">6</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Download to:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">15</property> + <child> + <object class="GtkBox" id="hbox_download_to_path_chooser"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">15</property> + <child> + <object class="GtkBox" id="hbox_move_completed_to_path_chooser"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment30"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">15</property> + <child> + <object class="GtkBox" id="hbox_copy_torrent_files_path_chooser"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Download Folders</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="newvbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_prioritize_first_last_pieces"> + <property name="label" translatable="yes">Prioritize first and last pieces of torrent</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Prioritize first and last pieces of files in torrent</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_sequential_download"> + <property name="label" translatable="yes">Sequential download</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">When enabled, the piece picker will pick pieces in +sequence instead of rarest first. + +Enabling sequential download will affect the piece +distribution negatively in the swarm. It should be +used sparingly.</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_add_paused"> + <property name="label" translatable="yes">Add torrents in Paused state</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_pre_allocation"> + <property name="label" translatable="yes">Pre-allocate disk space</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Pre-allocate the disk space for the torrent files</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Add Torrent Options</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_show_dialog"> + <property name="label" translatable="yes">Always show</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="chk_focus_dialog"> + <property name="label" translatable="yes">Bring the dialog to focus</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label47"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Add Torrents Dialog</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">3</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 7</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="BandwidthScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkGrid" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkSpinButton" id="spin_max_connections_per_second"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment_spin_max_conn_per_sec</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_half_open_connections"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment_spin_max_half_open_conn</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label58"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Connection Attempts per Second:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label57"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Half-Open Connections:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The maximum number of connections allowed. Set -1 for unlimited.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Connections:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The maximum upload slots for all torrents. Set -1 for unlimited.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Upload Slots:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_connections_global"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum number of connections allowed. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_conn_global</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + <property name="update_policy">if-valid</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The maximum download speed for all torrents. Set -1 for unlimited.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Download Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_download"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum download speed for all torrents. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_download</property> + <property name="climb_rate">1</property> + <property name="digits">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_upload"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum upload speed for all torrents. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_upload</property> + <property name="climb_rate">1</property> + <property name="digits">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_upload_slots_global"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum upload slots for all torrents. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_upload_slots_global</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">KiB/s</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">KiB/s</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label17"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The maximum upload speed for all torrents. Set -1 for unlimited.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Upload Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment28"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <child> + <object class="GtkCheckButton" id="chk_ignore_limits_on_local_network"> + <property name="label" translatable="yes">Ignore limits on local network</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment40"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">5</property> + <child> + <object class="GtkCheckButton" id="chk_rate_limit_ip_overhead"> + <property name="label" translatable="yes">Rate limit IP overhead</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, the estimated TCP/IP overhead is drained from the rate limiters, to avoid exceeding the limits with the total traffic</property> + <property name="halign">start</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label37"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Global Bandwidth Limits</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="bottom_padding">2</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid" id="table4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkSpinButton" id="spin_max_upload_slots_per_torrent"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum upload slots per torrent. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_upload_slots_per_torrent</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_connections_per_torrent"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum number of connections per torrent. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_conn_per_torrent</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Connections:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label24"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Upload Slots:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label31"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Download Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label32"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Upload Speed:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_download_per_torrent"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum number download speed per torrent. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_download_per_torrent</property> + <property name="digits">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_max_upload_per_torrent"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">The maximum upload speed per torrent. Set -1 for unlimited.</property> + <property name="adjustment">adjustment_spin_max_upload_per_torrent</property> + <property name="digits">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">KiB/s</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="label" translatable="yes">KiB/s</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label33"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Per-Torrent Bandwidth Limits</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label38"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 8</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="QueueScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <child> + <object class="GtkBox" id="queue_prefs_box2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkFrame" id="frame10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_queue_new_top"> + <property name="label" translatable="yes">Queue to top</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label36"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">New Torrents</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkGrid" id="table3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkSpinButton" id="spin_active"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment_spin_active</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_seeding"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment_spin_seeding</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label27"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Seeding:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label48"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Total:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_downloading"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment_spin_downloading</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label42"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Downloading:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_dont_count_slow_torrents"> + <property name="label" translatable="yes">Ignore slow torrents</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Torrents not transferring any data do not count towards download/seeding active count.</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_auto_manage_prefer_seeds"> + <property name="label" translatable="yes">Prefer seeding torrents</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Give preference to seeding torrents over downloading torrents.</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label49"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Active Torrents</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment24"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="label53"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Share Ratio:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label54"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Time Ratio:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label55"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Time (m):</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_seed_time_limit"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">adjustment_spin_seed_time_limit</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_seed_time_ratio_limit"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">adjustment_time_ratio_limit</property> + <property name="digits">2</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_share_ratio_limit"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">adjustment_share_ratio_limit</property> + <property name="digits">2</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label50"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Seeding Rotation</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="chk_share_ratio"> + <property name="label" translatable="yes">Share Ratio:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_share_ratio"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="invisible_char">•</property> + <property name="adjustment">adjustment_share_ratio</property> + <property name="digits">2</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">10</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="vbox8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="radio_pause_ratio"> + <property name="label" translatable="yes">Pause Torrent</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="radio_remove_ratio"> + <property name="label" translatable="yes">Remove Torrent</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <property name="group">radio_pause_ratio</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">3</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label25"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Share Ratio Reached</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label64"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 10</property> + </object> + <packing> + <property name="position">3</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="NetworkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame31"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment51"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkEntry" id="entry_interface"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">IP address or network interface name to listen for incoming BitTorrent connections. Leave empty to use system default.</property> + <property name="max_length">40</property> + <property name="width_chars">15</property> + <property name="truncate_multiline">True</property> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label110"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Incoming Interface</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="spin_incoming_port"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="max_length">5</property> + <property name="max_width_chars">6</property> + <property name="adjustment">adjustment_spin_incoming_port</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_random_incoming_port"> + <property name="label" translatable="yes">Random</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Uses random ports in range 49152 to 65525</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Active Port:</property> + <property name="justify">right</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="active_port_label"> + <property name="width_request">50</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">0</property> + <property name="width_chars">5</property> + <attributes> + <attribute name="style" value="italic"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_testport"> + <property name="label" translatable="yes">Test Active Port</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="image_position">right</property> + <signal name="clicked" handler="on_test_port_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment31"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkSpinner" id="port_spinner"> + <property name="visible">False</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment48"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkImage" id="port_img"> + <property name="can_focus">False</property> + <property name="icon_name">dialog-question-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Incoming Port</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment17"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkEntry" id="entry_outgoing_interface"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes"> + IP address or network interface name for outgoing BitTorrent connections. Leave empty to use system default. + </property> + <property name="max_length">40</property> + <property name="invisible_char">●</property> + <property name="width_chars">15</property> + <property name="truncate_multiline">True</property> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Outgoing Interface</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame26"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment34"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="chk_random_outgoing_ports"> + <property name="label" translatable="yes">Random</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Uses random ports in range 49152 to 65525</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_toggle" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="label77"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">From:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_outgoing_port_min"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="max_length">5</property> + <property name="width_chars">7</property> + <property name="adjustment">adjustment_spin_outgoing_port_min</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label78"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">To:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_outgoing_port_max"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="max_length">5</property> + <property name="width_chars">7</property> + <property name="adjustment">adjustment_spin_outgoing_port_max</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label79"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Outgoing Ports</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">10</property> + <child> + <object class="GtkBox" id="hbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkGrid" id="table7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkComboBox" id="combo_encout"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">liststore3</property> + <signal name="changed" handler="on_combo_encryption_changed" swapped="no"/> + <child> + <object class="GtkCellRendererText" id="cellrenderertext3"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combo_encin"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">liststore1</property> + <signal name="changed" handler="on_combo_encryption_changed" swapped="no"/> + <child> + <object class="GtkCellRendererText" id="cellrenderertext1"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Outgoing:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Incoming:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="hbox15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Level:</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combo_enclevel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="model">liststore2</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext2"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Encryption</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">4</property> + <property name="bottom_padding">2</property> + <property name="left_padding">10</property> + <child> + <object class="GtkGrid" id="table8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">1</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkCheckButton" id="chk_upnp"> + <property name="label" translatable="yes">UPnP</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Universal Plug and Play</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_natpmp"> + <property name="label" translatable="yes">NAT-PMP</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">NAT Port Mapping Protocol</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_utpex"> + <property name="label" translatable="yes">Peer Exchange</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Exchanges peers between clients. (Disabling requires restart)</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_lsd"> + <property name="label" translatable="yes">LSD</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Local Service Discovery finds local peers on your network.</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_dht"> + <property name="label" translatable="yes">DHT</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Distributed hash table may improve the amount of active connections.</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label_peer_tos"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Peer TOS Byte:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_peer_tos"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_length">4</property> + <property name="invisible_char">•</property> + <property name="width_chars">1</property> + <property name="text">0x00</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Network Extras</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">5</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">4</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label65"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 11</property> + </object> + <packing> + <property name="position">4</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="ProxyScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox26"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkFrame" id="frame_proxy"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment29"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkGrid" id="table11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label_proxy_pass"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Password:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_proxy_pass"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="visibility">False</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_proxy_host"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Hostname:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_proxy_host"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="truncate_multiline">True</property> + <signal name="paste-clipboard" handler="on_entry_proxy_host_paste_clipboard" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_proxy_port"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Port:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_proxy_port"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="spin_proxy_port"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment_spin_proxy_port</property> + <property name="numeric">True</property> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_proxy_user"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="combo_proxy_type"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="model">liststore_proxy</property> + <signal name="changed" handler="on_combo_proxy_type_changed" swapped="no"/> + <child> + <object class="GtkCellRendererText" id="cellrenderertext4"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_proxy_user"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Username:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_proxy_host_resolve"> + <property name="label" translatable="yes">Proxy Hostnames</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Hostnames should be attempted to be resolved through +the proxy instead of using the local DNS service</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_proxy_peer_conn"> + <property name="label" translatable="yes">Proxy Peers</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Proxy peer and web seed connections.</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">6</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_proxy_tracker_conn"> + <property name="label" translatable="yes">Proxy Trackers</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">7</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label_proxy"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Proxy</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame_anon_mode"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkAlignment" id="alignment_force_proxy"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkCheckButton" id="chk_force_proxy"> + <property name="label" translatable="yes">Force Proxy Use</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment_anon_mode"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <property name="right_padding">12</property> + <child> + <object class="GtkCheckButton" id="chk_anonymous_mode"> + <property name="label" translatable="yes">Hide Client Identity</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">Attempt to hide client identity and only use proxy for incoming connections.</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label_force_proxy"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Force Proxy</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">5</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label71"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 11</property> + </object> + <packing> + <property name="position">5</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="CacheScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox31"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame32"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment53"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid" id="table14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label114"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Cache Size (16 KiB blocks):</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label115"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">The number of seconds from the last cached write to a piece in the write cache, to when it's forcefully flushed to disk. Default is 60 seconds.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Cache Expiry (seconds):</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_cache_size"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + <property name="adjustment">adjustment_cache_size</property> + <property name="numeric">True</property> + <property name="update_policy">if-valid</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_cache_expiry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_length">5</property> + <property name="invisible_char">●</property> + <property name="adjustment">adjustment_cache_expiry</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label112"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Settings</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame33"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment54"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox32"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame34"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment55"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid" id="table18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label116"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The total number of 16 KiB blocks written to disk since this session was started.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Blocks Written:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label120"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The total number of write operations performed since this session was started.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Writes:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label124"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The ratio (blocks_written - writes) / blocks_written represents the number of saved write operations per total write operations, i.e. a kind of cache hit ratio for the write cache.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Write Cache Hit Ratio:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_num_blocks_written"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_write_ops"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_write_hit_ratio"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label132"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Write</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame35"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment56"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid" id="table19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label118"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The number of blocks that were requested from the bittorrent engine (from peers), that were served from disk or cache.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Blocks Read:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label122"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The cache hit ratio for the read cache.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Read Cache Hit Ratio:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_num_blocks_read"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_read_hit_ratio"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label121"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="has_tooltip">True</property> + <property name="tooltip_text" translatable="yes">The total number of read operations performed since this session was started.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Reads:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_read_ops"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label133"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Read</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame36"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment57"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid" id="table20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label123"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The number of 16 KiB blocks currently in the disk cache. This includes both read and write cache.</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Cache Size:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label117"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Read Cache Size:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_disk_blocks_in_use"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_cache_read_cache_blocks"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label134"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Size</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="hbuttonbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="button_cache_refresh"> + <property name="label" translatable="yes">_Refresh</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_cache_refresh_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label113"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Status</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">6</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label72"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label">page 12</property> + </object> + <packing> + <property name="position">6</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="OtherScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment37"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="left_padding">12</property> + <property name="right_padding">40</property> + <child> + <object class="GtkBox" id="vbox19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkLabel" id="label44"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Help us improve Deluge by sending us your Python version, PyGTK version, OS and processor types. Absolutely no other information is sent.</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">2</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_send_info"> + <property name="label" translatable="yes">Yes, please send anonymous statistics</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label45"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">System Information</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame30"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment49"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="box20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label109"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Location:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_geoip"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" 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">●</property> + <property name="truncate_multiline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label108"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">GeoIP Database</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame17"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment25"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="left_padding">12</property> + <child> + <object class="GtkButtonBox" id="hbuttonbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="button_associate_magnet"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_associate_magnet_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <child> + <object class="GtkImage" id="image_magnet"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">image-missing</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label60"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Associate with Deluge</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Magnet Links</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">7</property> + </packing> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <object class="GtkScrolledWindow" id="DaemonScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">2</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="hbox12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label41"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Daemon port:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_daemon_port"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_width_chars">6</property> + <property name="adjustment">adjustment_spin_daemon_port</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label35"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Port</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment21"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">10</property> + <child> + <object class="GtkCheckButton" id="chk_allow_remote_connections"> + <property name="label" translatable="yes">Allow Remote Connections</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label39"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Connections</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment27"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">10</property> + <child> + <object class="GtkCheckButton" id="chk_new_releases"> + <property name="label" translatable="yes">Periodically check the website for new releases</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label56"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Other</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="AccountsFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment33"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <property name="right_padding">20</property> + <child> + <object class="GtkBox" id="hbox22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkButtonBox" id="vbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <property name="homogeneous">True</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="accounts_add"> + <property name="label" translatable="yes">_Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_accounts_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="accounts_edit"> + <property name="label" translatable="yes">_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_underline">True</property> + <signal name="clicked" handler="on_accounts_edit_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="accounts_delete"> + <property name="label" translatable="yes">_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_underline">True</property> + <signal name="clicked" handler="on_accounts_delete_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">4</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <property name="min_content_height">150</property> + <child> + <object class="GtkTreeView" id="accounts_listview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection2"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label61"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Accounts</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">8</property> + </packing> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <object class="GtkScrolledWindow" id="PluginsScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox28"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkPaned" id="vpaned1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow12"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="plugin_listview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection3"/> + </child> + </object> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow11"> + <property name="height_request">115</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkViewport" id="viewport12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkFrame" id="frame8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment26"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkGrid" id="table10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label_plugin_details"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_plugin_version"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_plugin_author"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label85"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Details:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label84"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Version:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label82"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Author:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label86"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Homepage:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label87"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Author Email:</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_plugin_homepage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_plugin_email"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label81"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Info</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">center</property> + <child> + <object class="GtkButton" id="button_plugin_install"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_plugin_install_clicked" swapped="no"/> + <child> + <object class="GtkLabel" id="label28"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Install</property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_rescan_plugins"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_rescan_plugins_clicked" swapped="no"/> + <child> + <object class="GtkLabel" id="label30"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Refresh</property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_find_plugins"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_button_find_plugins_clicked" swapped="no"/> + <child> + <object class="GtkLabel" id="label107"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Find More...</property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="position">9</property> + </packing> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_cancel</action-widget> + <action-widget response="0">button_apply</action-widget> + <action-widget response="0">button_ok</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/queuedtorrents.ui b/deluge/ui/gtk3/glade/queuedtorrents.ui new file mode 100644 index 0000000..528ef64 --- /dev/null +++ b/deluge/ui/gtk3/glade/queuedtorrents.ui @@ -0,0 +1,211 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="queued_torrents_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Queued Torrents</property> + <property name="window_position">center-on-parent</property> + <property name="default_width">450</property> + <property name="default_height">300</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_close"> + <property name="label" translatable="yes">_Close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_close_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_add"> + <property name="label" translatable="yes">_Add</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="is_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Add Queued Torrents</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeselection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="button_remove"> + <property name="label" translatable="yes">_Remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_remove_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_clear"> + <property name="label" translatable="yes">_Clear</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_button_clear_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_autoadd"> + <property name="label" translatable="yes">Automatically add torrents on connect</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="halign">start</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_chk_autoadd_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">button_close</action-widget> + <action-widget response="0">button_add</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/remove_torrent_dialog.ui b/deluge/ui/gtk3/glade/remove_torrent_dialog.ui new file mode 100644 index 0000000..4a92037 --- /dev/null +++ b/deluge/ui/gtk3/glade/remove_torrent_dialog.ui @@ -0,0 +1,187 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkDialog" id="remove_torrent_dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Remove Torrent</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <placeholder/> + </child> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="cancel_button"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="ok_button"> + <property name="label" translatable="yes">_OK</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">dialog-warning-symbolic</property> + <property name="icon_size">6</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_title"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Remove the selected torrent(s)?</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.3"/> + </attributes> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_torrents"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="ellipsize">end</property> + <attributes> + <attribute name="style" value="italic"/> + </attributes> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <object class="GtkSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckButton" id="delete_files"> + <property name="label" translatable="yes">Include downloaded files</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="focus_on_click">False</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_delete_files_toggled" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="warning_label"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">(This is permanent!)</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="0.90000000000000002"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">1</property> + <property name="position">5</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">cancel_button</action-widget> + <action-widget response="-5">ok_button</action-widget> + </action-widgets> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/torrent_menu.options.ui b/deluge/ui/gtk3/glade/torrent_menu.options.ui new file mode 100644 index 0000000..df34445 --- /dev/null +++ b/deluge/ui/gtk3/glade/torrent_menu.options.ui @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkImage" id="download-limit-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">deluge-downloading</property> + </object> + <object class="GtkImage" id="max-connections-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">network-transmit-receive-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="upload-limit-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">deluge-seeding</property> + </object> + <object class="GtkImage" id="upload-slots-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">view-sort-descending-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkMenu" id="options_torrent_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="menuitem_down_speed"> + <property name="label" translatable="yes">_Download Speed Limit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">download-limit-image</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_up_speed"> + <property name="label" translatable="yes">_Upload Speed Limit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">upload-limit-image</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_max_connections"> + <property name="label" translatable="yes">_Connection Limit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">max-connections-image</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_upload_slots"> + <property name="label" translatable="yes">Upload _Slot Limit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">upload-slots-image</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menuitem_stop_seed_at_ratio"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Stop seed at _ratio</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menuitem_auto_managed"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Auto Managed</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menuitem_super_seeding"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_action_appearance">False</property> + <property name="label" translatable="yes">_Super Seeding</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="menuitem_change_owner"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Change Ownership</property> + <property name="use_underline">True</property> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/torrent_menu.queue.ui b/deluge/ui/gtk3/glade/torrent_menu.queue.ui new file mode 100644 index 0000000..a120af2 --- /dev/null +++ b/deluge/ui/gtk3/glade/torrent_menu.queue.ui @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkImage" id="image5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-top-symbolic</property> + </object> + <object class="GtkImage" id="image6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-up-symbolic</property> + </object> + <object class="GtkImage" id="image7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-down-symbolic</property> + </object> + <object class="GtkImage" id="image8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-bottom-symbolic</property> + </object> + <object class="GtkMenu" id="queue_torrent_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="menuitem_queue_top"> + <property name="label" translatable="yes">Top</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="image">image5</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_queue_top_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_queue_up"> + <property name="label" translatable="yes">Up</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="image">image6</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_queue_up_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_queue_down"> + <property name="label" translatable="yes">Down</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="image">image7</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_queue_down_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_queue_bottom"> + <property name="label" translatable="yes">Bottom</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="image">image8</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_queue_bottom_activate" swapped="no"/> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/torrent_menu.ui b/deluge/ui/gtk3/glade/torrent_menu.ui new file mode 100644 index 0000000..c9ee289 --- /dev/null +++ b/deluge/ui/gtk3/glade/torrent_menu.ui @@ -0,0 +1,241 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAccelGroup" id="accelgroup1"/> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">preferences-system-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image1"> + <property name="can_focus">False</property> + <property name="icon_name">media-playback-stop-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-edit-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">media-playback-start-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image14"> + <property name="can_focus">False</property> + <property name="icon_name">media-playback-pause-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image15"> + <property name="can_focus">False</property> + <property name="icon_name">edit-copy-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">media-playlist-shuffle-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">media-playlist-repeat-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">view-refresh-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">edit-redo-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">folder-open-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-save-as-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="menu-item-image9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-remove-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkMenu" id="torrent_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="menuitem_open_folder"> + <property name="label" translatable="yes">_Open Download Folder</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image7</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_open_folder_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_pause"> + <property name="label" translatable="yes">_Pause</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image14</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_pause_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_resume"> + <property name="label" translatable="yes">Resu_me</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Resume selected torrents.</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image13</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_resume_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_options"> + <property name="label" translatable="yes">Opt_ions</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">image1</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator_menuitem16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_queue"> + <property name="label" translatable="yes">_Queue</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image19</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_copymagnet"> + <property name="label" translatable="yes">_Copy Magnet URI</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image15</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_copymagnet_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_updatetracker"> + <property name="label" translatable="yes">_Update Tracker</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image5</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_updatetracker_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_edittrackers"> + <property name="label" translatable="yes">_Edit Trackers</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image12</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_edittrackers_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_remove"> + <property name="label" translatable="yes">_Remove Torrent</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image9</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_remove_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_recheck"> + <property name="label" translatable="yes">_Force Re-check</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image6</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_recheck_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_move"> + <property name="label" translatable="yes">_Move Download Folder</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">menu-item-image8</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_move_activate" swapped="no"/> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/glade/tray_menu.ui b/deluge/ui/gtk3/glade/tray_menu.ui new file mode 100644 index 0000000..060b7a0 --- /dev/null +++ b/deluge/ui/gtk3/glade/tray_menu.ui @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAccelGroup" id="accelgroup1"/> + <object class="GtkImage" id="add-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="download-limit-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="pause-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">media-playback-stop-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="quit-daemon-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">system-shutdown-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="quit-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">application-exit-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="resume-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">media-playlist-repeat-symbolic</property> + <property name="icon_size">1</property> + </object> + <object class="GtkImage" id="upload-limit-image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_size">1</property> + </object> + <object class="GtkMenu" id="tray_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckMenuItem" id="menuitem_show_deluge"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Show Deluge</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_menuitem_show_deluge_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_add_torrent"> + <property name="label" translatable="yes">_Add Torrent</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">add-image</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_add_torrent_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_pause_session"> + <property name="label" translatable="yes">_Pause Session</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">pause-image</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_pause_session_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_resume_session"> + <property name="label" translatable="yes">_Resume Session</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">resume-image</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_resume_session_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_download_limit"> + <property name="label" translatable="yes">_Download Speed Limit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">download-limit-image</property> + <property name="use_stock">False</property> + <property name="always_show_image">True</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_upload_limit"> + <property name="label" translatable="yes">_Upload Speed Limit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">upload-limit-image</property> + <property name="use_stock">False</property> + <property name="always_show_image">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_quitdaemon"> + <property name="label" translatable="yes">Quit & Shutdown Daemon</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">quit-daemon-image</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_menuitem_quitdaemon_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separatormenuitem5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="menuitem_quit"> + <property name="label" translatable="yes">_Quit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="image">quit-image</property> + <property name="use_stock">False</property> + <property name="accel_group">accelgroup1</property> + <signal name="activate" handler="on_menuitem_quit_activate" swapped="no"/> + </object> + </child> + </object> +</interface> diff --git a/deluge/ui/gtk3/gtkui.py b/deluge/ui/gtk3/gtkui.py new file mode 100644 index 0000000..73a7c97 --- /dev/null +++ b/deluge/ui/gtk3/gtkui.py @@ -0,0 +1,399 @@ +# +# Copyright (C) 2007-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=wrong-import-position + +import logging +import os +import signal +import sys +import time + +import gi # isort:skip (Required before Gtk import). + +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') + +# isort:imports-thirdparty +from gi.repository.GLib import set_prgname +from gi.repository.Gtk import Builder, ResponseType +from twisted.internet import defer, gtk3reactor +from twisted.internet.error import ReactorAlreadyInstalledError +from twisted.internet.task import LoopingCall + +try: + # Install twisted reactor, before any other modules import reactor. + reactor = gtk3reactor.install() +except ReactorAlreadyInstalledError: + # Running unit tests so already installed a rector + from twisted.internet import reactor + +# isort:imports-firstparty +import deluge.component as component +from deluge.common import ( + fsize, + fspeed, + get_default_download_dir, + osx_check, + windows_check, +) +from deluge.configmanager import ConfigManager, get_config_dir +from deluge.error import DaemonRunningError, LibtorrentImportError +from deluge.i18n import I18N_DOMAIN, set_language, setup_translation +from deluge.ui.client import client +from deluge.ui.hostlist import LOCALHOST +from deluge.ui.sessionproxy import SessionProxy +from deluge.ui.tracker_icons import TrackerIcons + +# isort:imports-localfolder +from .addtorrentdialog import AddTorrentDialog +from .common import associate_magnet_links, windowing +from .connectionmanager import ConnectionManager +from .dialogs import YesNoDialog +from .filtertreeview import FilterTreeView +from .ipcinterface import IPCInterface, process_args +from .mainwindow import MainWindow +from .menubar import MenuBar +from .pluginmanager import PluginManager +from .preferences import Preferences +from .queuedtorrents import QueuedTorrents +from .sidebar import SideBar +from .statusbar import StatusBar +from .systemtray import SystemTray +from .toolbar import ToolBar +from .torrentdetails import TorrentDetails +from .torrentview import TorrentView + +set_prgname('deluge') +log = logging.getLogger(__name__) + +try: + from setproctitle import getproctitle, setproctitle +except ImportError: + + def setproctitle(title): + return + + def getproctitle(): + return + + +DEFAULT_PREFS = { + 'standalone': True, + 'prefer_dark_theme': False, + 'interactive_add': True, + 'focus_add_dialog': True, + 'enable_system_tray': True, + 'close_to_tray': False, + 'start_in_tray': False, + 'enable_appindicator': True, + 'lock_tray': False, + 'tray_password': '', + 'check_new_releases': True, + 'default_load_path': None, + 'window_maximized': False, + 'window_x_pos': 0, + 'window_y_pos': 0, + 'window_width': 640, + 'window_height': 480, + 'pref_dialog_width': None, + 'pref_dialog_height': None, + 'edit_trackers_dialog_width': None, + 'edit_trackers_dialog_height': None, + 'tray_download_speed_list': [5.0, 10.0, 30.0, 80.0, 300.0], + 'tray_upload_speed_list': [5.0, 10.0, 30.0, 80.0, 300.0], + 'connection_limit_list': [50, 100, 200, 300, 500], + 'enabled_plugins': [], + 'show_connection_manager_on_start': True, + 'autoconnect': False, + 'autoconnect_host_id': None, + 'autostart_localhost': False, + 'autoadd_queued': False, + 'choose_directory_dialog_path': get_default_download_dir(), + 'show_new_releases': True, + 'show_sidebar': True, + 'show_toolbar': True, + 'show_statusbar': True, + 'show_tabsbar': True, + 'tabsbar_tab_pos': 'top', + 'tabsbar_position': 235, + 'sidebar_show_zero': False, + 'sidebar_show_trackers': True, + 'sidebar_show_owners': True, + 'sidebar_position': 170, + 'show_rate_in_title': False, + 'createtorrent.trackers': [], + 'show_piecesbar': False, + 'detect_urls': True, + 'pieces_color_missing': [65535, 0, 0], + 'pieces_color_waiting': [4874, 56494, 0], + 'pieces_color_downloading': [65535, 55255, 0], + 'pieces_color_completed': [4883, 26985, 56540], + 'focus_main_window_on_add': True, + 'language': None, +} + + +class GtkUI: + def __init__(self, args): + # Setup gtkbuilder/glade translation + setup_translation() + Builder().set_translation_domain(I18N_DOMAIN) + + # Setup signals + def on_die(*args): + log.debug('OS signal "die" caught with args: %s', args) + reactor.stop() + + self.osxapp = None + if windows_check(): + from win32api import SetConsoleCtrlHandler + + SetConsoleCtrlHandler(on_die, True) + log.debug('Win32 "die" handler registered') + elif osx_check() and windowing('quartz'): + try: + import gtkosx_application + except ImportError: + pass + else: + self.osxapp = gtkosx_application.gtkosx_application_get() + self.osxapp.connect('NSApplicationWillTerminate', on_die) + log.debug('OSX quartz "die" handler registered') + + # Set process name again to fix gtk issue + setproctitle(getproctitle()) + + # Attempt to register a magnet URI handler with gconf, but do not overwrite + # if already set by another program. + associate_magnet_links(False) + + # Make sure gtk3ui.conf has at least the defaults set + self.config = ConfigManager('gtk3ui.conf', DEFAULT_PREFS) + + # Make sure the gtkui state folder has been created + if not os.path.exists(os.path.join(get_config_dir(), 'gtk3ui_state')): + os.makedirs(os.path.join(get_config_dir(), 'gtk3ui_state')) + + # Set language + if self.config['language'] is not None: + set_language(self.config['language']) + + # Start the IPC Interface before anything else.. Just in case we are + # already running. + self.queuedtorrents = QueuedTorrents() + self.ipcinterface = IPCInterface(args.torrents) + + # We make sure that the UI components start once we get a core URI + client.set_disconnect_callback(self.__on_disconnect) + + self.trackericons = TrackerIcons() + self.sessionproxy = SessionProxy() + # Initialize various components of the gtkui + self.mainwindow = MainWindow() + self.menubar = MenuBar() + self.toolbar = ToolBar() + self.torrentview = TorrentView() + self.torrentdetails = TorrentDetails() + self.sidebar = SideBar() + self.filtertreeview = FilterTreeView() + self.preferences = Preferences() + self.systemtray = SystemTray() + self.statusbar = StatusBar() + self.addtorrentdialog = AddTorrentDialog() + + if self.osxapp: + + def nsapp_open_file(osxapp, filename): + # Ignore command name which is raised at app launch (python opening main script). + if filename == sys.argv[0]: + return True + process_args([filename]) + + self.osxapp.connect('NSApplicationOpenFile', nsapp_open_file) + from .menubar_osx import menubar_osx + + menubar_osx(self, self.osxapp) + self.osxapp.ready() + + # Initialize the plugins + self.plugins = PluginManager() + + # Show the connection manager + self.connectionmanager = ConnectionManager() + + # Setup RPC stats logging + # daemon_bps: time, bytes_sent, bytes_recv + self.daemon_bps = (0, 0, 0) + self.rpc_stats = LoopingCall(self.log_rpc_stats) + self.closing = False + + # Twisted catches signals to terminate, so have it call a pre_shutdown method. + reactor.addSystemEventTrigger('before', 'gtkui_close', self.close) + + def gtkui_sigint_handler(num, frame): + log.debug('SIGINT signal caught, firing event: gtkui_close') + reactor.callLater(0, reactor.fireSystemEvent, 'gtkui_close') + + signal.signal(signal.SIGINT, gtkui_sigint_handler) + + def start(self): + reactor.callWhenRunning(self._on_reactor_start) + reactor.run() + # Reactor is not running. Any async callbacks (Deferreds) can no longer + # be processed from this point on. + + def shutdown(self, *args, **kwargs): + log.debug('GTKUI shutting down...') + # Shutdown all components + if client.is_standalone: + return component.shutdown() + + @defer.inlineCallbacks + def close(self): + if self.closing: + return + self.closing = True + # Make sure the config is saved. + self.config.save() + # Ensure columns state is saved + self.torrentview.save_state() + # Shut down components + yield self.shutdown() + + # The gtk modal dialogs (e.g. Preferences) can prevent the application + # quitting, so force exiting by destroying MainWindow. Must be done here + # to avoid hanging when quitting with SIGINT (CTRL-C). + self.mainwindow.window.destroy() + + reactor.stop() + + # Restart the application after closing if MainWindow restart attribute set. + if component.get('MainWindow').restart: + os.execv(sys.argv[0], sys.argv) + + def log_rpc_stats(self): + """Log RPC statistics for thinclient mode.""" + if not client.connected(): + return + + t = time.time() + recv = client.get_bytes_recv() + sent = client.get_bytes_sent() + delta_time = t - self.daemon_bps[0] + delta_sent = sent - self.daemon_bps[1] + delta_recv = recv - self.daemon_bps[2] + self.daemon_bps = (t, sent, recv) + sent_rate = fspeed(delta_sent / delta_time) + recv_rate = fspeed(delta_recv / delta_time) + log.debug( + 'RPC: Sent %s (%s) Recv %s (%s)', + fsize(sent), + sent_rate, + fsize(recv), + recv_rate, + ) + + def _on_reactor_start(self): + log.debug('_on_reactor_start') + self.mainwindow.first_show() + + if not self.config['standalone']: + return self._start_thinclient() + + err_msg = '' + try: + client.start_standalone() + except DaemonRunningError: + err_msg = _( + 'A Deluge daemon (deluged) is already running.\n' + 'To use Standalone mode, stop local daemon and restart Deluge.' + ) + except LibtorrentImportError as ex: + if 'libtorrent library not found' in str(ex): + err_msg = _( + 'Only Thin Client mode is available because libtorrent is not installed.\n' + 'To use Standalone mode, please install libtorrent package.' + ) + else: + log.exception(ex) + err_msg = _( + 'Only Thin Client mode is available due to libtorrent import error: %s\n' + 'To use Standalone mode, please see logs for error details.' + % (str(ex)) + ) + + except ImportError as ex: + log.exception(ex) + err_msg = _( + 'Only Thin Client mode is available due to unknown Import Error.\n' + 'To use Standalone mode, please see logs for error details.' + ) + except Exception as ex: + log.exception(ex) + err_msg = _( + 'Only Thin Client mode is available due to unknown Import Error.\n' + 'To use Standalone mode, please see logs for error details.' + ) + else: + component.start() + return + + def on_dialog_response(response): + """User response to switching mode dialog.""" + if response == ResponseType.YES: + # Turning off standalone + self.config['standalone'] = False + self._start_thinclient() + else: + # User want keep Standalone Mode so just quit. + self.mainwindow.quit() + + # An error occurred so ask user to switch from Standalone to Thin Client mode. + err_msg += '\n\n' + _('Continue in Thin Client mode?') + d = YesNoDialog(_('Change User Interface Mode'), err_msg).run() + d.addCallback(on_dialog_response) + + def _start_thinclient(self): + """Start the gtkui in thinclient mode""" + if log.isEnabledFor(logging.DEBUG): + self.rpc_stats.start(10) + + # Check to see if we need to start the localhost daemon + if self.config['autostart_localhost']: + + def on_localhost_status(status_info, port): + if status_info[1] == 'Offline': + log.debug('Autostarting localhost: %s', host_config[0:3]) + self.connectionmanager.start_daemon(port, get_config_dir()) + + for host_config in self.connectionmanager.hostlist.config['hosts']: + if host_config[1] in LOCALHOST: + d = self.connectionmanager.hostlist.get_host_status(host_config[0]) + d.addCallback(on_localhost_status, host_config[2]) + break + + # Autoconnect to a host + if self.config['autoconnect']: + for host_config in self.connectionmanager.hostlist.config['hosts']: + host_id, host, port, user, __ = host_config + if host_id == self.config['autoconnect_host_id']: + log.debug('Trying to connect to %s@%s:%s', user, host, port) + self.connectionmanager._connect(host_id, try_counter=6) + break + + if self.config['show_connection_manager_on_start']: + # Dialog is blocking so call last. + self.connectionmanager.show() + + def __on_disconnect(self): + """ + Called when disconnected from the daemon. We basically just stop all + the components here. + """ + self.daemon_bps = (0, 0, 0) + component.stop() diff --git a/deluge/ui/gtk3/ipcinterface.py b/deluge/ui/gtk3/ipcinterface.py new file mode 100644 index 0000000..0ef28d8 --- /dev/null +++ b/deluge/ui/gtk3/ipcinterface.py @@ -0,0 +1,221 @@ +# +# Copyright (C) 2008-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. +# + +import logging +import os +import sys +from base64 import b64encode +from glob import glob +from tempfile import mkstemp +from urllib.parse import urlparse +from urllib.request import url2pathname + +import rencode +import twisted.internet.error +from twisted.internet import reactor +from twisted.internet.protocol import ClientFactory, Factory, Protocol, connectionDone + +import deluge.component as component +from deluge.common import decode_bytes, is_magnet, is_url, windows_check +from deluge.configmanager import ConfigManager, get_config_dir +from deluge.ui.client import client + +log = logging.getLogger(__name__) + + +class IPCProtocolServer(Protocol): + def __init__(self): + pass + + def dataReceived(self, data): # NOQA: N802 + config = ConfigManager('gtk3ui.conf') + data = rencode.loads(data, decode_utf8=True) + if not data or config['focus_main_window_on_add']: + component.get('MainWindow').present() + process_args(data) + + +class IPCProtocolClient(Protocol): + def __init__(self): + pass + + def connectionMade(self): # NOQA: N802 + self.transport.write(rencode.dumps(self.factory.args)) + self.transport.loseConnection() + + def connectionLost(self, reason=connectionDone): # NOQA: N802 + reactor.stop() + self.factory.stop = True + + +class IPCClientFactory(ClientFactory): + protocol = IPCProtocolClient + + def __init__(self): + self.stop = False + + def clientConnectionFailed(self, connector, reason): # NOQA: N802 + log.warning('Connection to running instance failed.') + reactor.stop() + + +class IPCInterface(component.Component): + def __init__(self, args): + component.Component.__init__(self, 'IPCInterface') + self.listener = None + ipc_dir = get_config_dir('ipc') + if not os.path.exists(ipc_dir): + os.makedirs(ipc_dir) + socket = os.path.join(ipc_dir, 'deluge-gtk') + if windows_check(): + # If we're on windows we need to check the global mutex to see if deluge is + # already running. + import win32api + import win32event + import winerror + + self.mutex = win32event.CreateMutex(None, False, 'deluge') + if win32api.GetLastError() != winerror.ERROR_ALREADY_EXISTS: + # Create listen socket + self.factory = Factory() + self.factory.protocol = IPCProtocolServer + import random + + port = random.randrange(20000, 65535) + self.listener = reactor.listenTCP(port, self.factory) + # Store the port number in the socket file + with open(socket, 'w') as _file: + _file.write(str(port)) + # We need to process any args when starting this process + process_args(args) + else: + # Send to existing deluge process + with open(socket) as _file: + port = int(_file.readline()) + self.factory = ClientFactory() + self.factory.args = args + self.factory.protocol = IPCProtocolClient + reactor.connectTCP('127.0.0.1', port, self.factory) + reactor.run() + sys.exit(0) + else: + # Find and remove any restart tempfiles + restart_tempfile = glob(os.path.join(ipc_dir, 'restart.*')) + for f in restart_tempfile: + os.remove(f) + lockfile = socket + '.lock' + log.debug('Checking if lockfile exists: %s', lockfile) + if os.path.lexists(lockfile): + + def delete_lockfile(): + log.debug('Delete stale lockfile.') + try: + os.remove(lockfile) + os.remove(socket) + except OSError as ex: + log.error('Failed to delete lockfile: %s', ex) + + try: + os.kill(int(os.readlink(lockfile)), 0) + except OSError: + delete_lockfile() + else: + if restart_tempfile: + log.warning( + 'Found running PID but it is not a Deluge process, removing lockfile...' + ) + delete_lockfile() + try: + self.factory = Factory() + self.factory.protocol = IPCProtocolServer + self.listener = reactor.listenUNIX(socket, self.factory, wantPID=True) + except twisted.internet.error.CannotListenError as ex: + log.info( + 'Deluge is already running! Sending arguments to running instance...' + ) + self.factory = IPCClientFactory() + self.factory.args = args + reactor.connectUNIX(socket, self.factory, checkPID=True) + reactor.run() + if self.factory.stop: + log.info('Success sending arguments to running Deluge.') + from gi.repository.Gdk import notify_startup_complete + + notify_startup_complete() + sys.exit(0) + else: + if restart_tempfile: + log.error('Deluge restart failed: %s', ex) + sys.exit(1) + else: + log.warning('Restarting Deluge... (%s)', ex) + # Create a tempfile to keep track of restart + mkstemp(prefix='restart.', dir=ipc_dir) + os.execv(sys.argv[0], sys.argv) + else: + process_args(args) + + def shutdown(self): + if windows_check(): + import win32api + + win32api.CloseHandle(self.mutex) + if self.listener: + return self.listener.stopListening() + + +def process_args(args): + """Process arguments sent to already running Deluge""" + # Make sure args is a list + args = list(args) + log.debug('Processing args from other process: %s', args) + if not client.connected(): + # We're not connected so add these to the queue + log.debug('Not connected to host.. Adding to queue.') + component.get('QueuedTorrents').add_to_queue(args) + return + config = ConfigManager('gtk3ui.conf') + + for arg in args: + if not arg.strip(): + continue + log.debug('arg: %s', arg) + + if is_url(arg): + log.debug('Attempting to add url (%s) from external source...', arg) + if config['interactive_add']: + component.get('AddTorrentDialog').add_from_url(arg) + component.get('AddTorrentDialog').show(config['focus_add_dialog']) + else: + client.core.add_torrent_url(arg, None) + + elif is_magnet(arg): + log.debug('Attempting to add magnet (%s) from external source...', arg) + if config['interactive_add']: + component.get('AddTorrentDialog').add_from_magnets([arg]) + component.get('AddTorrentDialog').show(config['focus_add_dialog']) + else: + client.core.add_torrent_magnet(arg, {}) + + else: + log.debug('Attempting to add file (%s) from external source...', arg) + if urlparse(arg).scheme == 'file': + arg = url2pathname(urlparse(arg).path) + path = os.path.abspath(decode_bytes(arg)) + + if not os.path.exists(path): + log.error('No such file: %s', path) + continue + + if config['interactive_add']: + component.get('AddTorrentDialog').add_from_files([path]) + component.get('AddTorrentDialog').show(config['focus_add_dialog']) + else: + with open(path, 'rb') as _file: + filedump = b64encode(_file.read()) + client.core.add_torrent_file(os.path.split(path)[-1], filedump, None) diff --git a/deluge/ui/gtk3/listview.py b/deluge/ui/gtk3/listview.py new file mode 100644 index 0000000..a80d795 --- /dev/null +++ b/deluge/ui/gtk3/listview.py @@ -0,0 +1,831 @@ +# +# Copyright (C) 2007, 2008 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. +# + +import logging + +from gi.repository import GObject, Gtk + +from deluge.common import decode_bytes + +from .common import cmp, load_pickled_state_file, save_pickled_state_file + +log = logging.getLogger(__name__) + + +class ListViewColumnState: + """Class used for saving/loading column state.""" + + def __init__(self, name, position, width, visible, sort, sort_order): + self.name = name + self.position = position + self.width = width + self.visible = visible + self.sort = sort + self.sort_order = sort_order + + +class ListView: + """ListView is used to make custom GtkTreeViews. It supports the adding + and removing of columns, creating a menu for a column toggle list and + support for 'status_field's which are used while updating the columns data. + """ + + class ListViewColumn: + """Holds information regarding a column in the ListView""" + + def __init__(self, name, column_indices): + # Name is how a column is identified and is also the header + self.name = name + # Column_indices holds the indexes to the liststore_columns that + # this column utilizes. It is stored as a list. + self.column_indices = column_indices + # Column is a reference to the GtkTreeViewColumn object + self.column = None + # This is the name of the status field that the column will query + # the core for if an update is called. + self.status_field = None + # If column is 'hidden' then it will not be visible and will not + # show up in any menu listing; it cannot be shown ever. + self.hidden = False + # If this is set, it is used to sort the model + self.sort_func = None + self.sort_id = None + # Values needed to update TreeViewColumns + self.column_type = None + self.renderer = None + self.text_index = 0 + self.value_index = 0 + self.pixbuf_index = 0 + self.data_func = None + + class TreeviewColumn(Gtk.TreeViewColumn): + """ + TreeViewColumn does not signal right-click events, and we need them + This subclass is equivalent to TreeViewColumn, but it signals these events + + Most of the code of this class comes from Quod Libet (http://www.sacredchao.net/quodlibet) + """ + + __gsignals__ = { + 'button-press-event': (GObject.SignalFlags.RUN_LAST, None, (object,)) + } + + def __init__(self, title=None, cell_renderer=None, **args): + """Constructor, see Gtk.TreeViewColumn""" + Gtk.TreeViewColumn.__init__(self, title, cell_renderer, **args) + label = Gtk.Label(label=title) + self.set_widget(label) + label.show() + label.__realize = label.connect('realize', self.on_realize) + self.title = title + self.data_func = None + self.data_func_data = None + self.cell_renderer = None + + def on_realize(self, widget): + widget.disconnect(widget.__realize) + del widget.__realize + button = widget.get_ancestor(Gtk.Button) + if button is not None: + button.connect('button-press-event', self.on_button_pressed) + + def on_button_pressed(self, widget, event): + self.emit('button-press-event', event) + + def set_cell_data_func_attributes(self, cell_renderer, func, func_data=None): + """Store the values to be set by set_cell_data_func""" + self.data_func = func + self.data_func_data = func_data + self.cell_renderer = cell_renderer + + def set_visible(self, visible): + Gtk.TreeViewColumn.set_visible(self, visible) + if self.data_func: + if not visible: + # Set data function to None to prevent unnecessary calls when column is hidden + self.set_cell_data_func(self.cell_renderer, None, func_data=None) + else: + self.set_cell_data_func( + self.cell_renderer, self.data_func, self.data_func_data + ) + + def set_col_attributes(self, renderer, add=True, **kw): + if add is True: + for attr, value in kw.items(): + self.add_attribute(renderer, attr, value) + else: + self.set_attributes(renderer, **kw) + + def __init__(self, treeview_widget=None, state_file=None): + log.debug('ListView initialized..') + + if treeview_widget is not None: + # User supplied a treeview widget + self.treeview = treeview_widget + else: + self.treeview = Gtk.TreeView() + + self.treeview.set_enable_search(True) + self.treeview.set_search_equal_func(self.on_keypress_search_by_name, None) + + if state_file: + self.load_state(state_file) + + self.liststore = None + self.model_filter = None + + self.treeview.set_reorderable(False) + self.treeview.set_rubber_banding(True) # Enable mouse multi-row selection. + self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + + # Dictionary of 'header' or 'name' to ListViewColumn object + self.columns = {} + # Column_index keeps track of the order of the visible columns. + self.column_index = [] + # The column types for the list store.. this may have more entries than + # visible columns due to some columns utilizing more than 1 liststore + # column and some columns being hidden. + self.liststore_columns = [] + # The GtkMenu that is created after every addition, removal or reorder + self.menu = None + # A list of menus that self.menu will be a submenu of everytime it is + # created. + self.checklist_menus = [] + + # Store removed columns state. This is needed for plugins that remove + # their columns prior to having the state list saved on shutdown. + self.removed_columns_state = [] + + # Since gtk TreeModelSort doesn't do stable sort, remember last sort order so we can + self.last_sort_order = {} + self.unique_column_id = None + self.default_sort_column_id = None + + # Create the model filter and column + self.add_bool_column('filter', hidden=True) + + def create_model_filter(self): + """create new filter-model + must be called after listview.create_new_liststore + """ + model_filter = self.liststore.filter_new() + model_filter.set_visible_column(self.columns['filter'].column_indices[0]) + self.model_filter = Gtk.TreeModelSort(model=model_filter) + self.model_filter.connect('sort-column-changed', self.on_model_sort_changed) + self.model_filter.connect('row-inserted', self.on_model_row_inserted) + self.treeview.set_model(self.model_filter) + self.set_sort_functions() + self.set_model_sort() + + def set_model_sort(self): + column_state = self.get_sort_column_from_state() + if column_state: + self.treeview.get_model().set_sort_column_id( + column_state.sort, column_state.sort_order + ) + # Using the default sort column + elif self.default_sort_column_id: + self.model_filter.set_sort_column_id( + self.default_sort_column_id, Gtk.SortType.ASCENDING + ) + self.model_filter.set_default_sort_func( + self.generic_sort_func, self.get_column_index(_('Added'))[0] + ) + + def get_sort_column_from_state(self): + """Find the first (should only be one) state with sort enabled""" + if self.state is None: + return None + for column_state in self.state: + if column_state.sort is not None and column_state.sort > -1: + return column_state + return None + + def on_model_sort_changed(self, model): + if self.unique_column_id: + self.last_sort_order = {} + + def record_position(model, path, _iter, data): + unique_id = model[_iter][self.unique_column_id] + self.last_sort_order[unique_id] = int(str(path)) + + model.foreach(record_position, None) + + def on_model_row_inserted(self, model, path, _iter): + if self.unique_column_id: + self.last_sort_order.setdefault( + model[_iter][self.unique_column_id], len(model) - 1 + ) + + def stabilize_sort_func(self, sort_func): + def stabilized(model, iter1, iter2, data): + result = sort_func(model, iter1, iter2, data) + if result == 0 and self.unique_column_id: + unique1 = model[iter1][self.unique_column_id] + unique2 = model[iter2][self.unique_column_id] + if unique1 in self.last_sort_order and unique2 in self.last_sort_order: + result = cmp( + self.last_sort_order[unique1], self.last_sort_order[unique2] + ) + # If all else fails, fall back to sorting by unique column + if result == 0: + result = cmp(unique1, unique2) + + return result + + return stabilized + + def generic_sort_func(self, model, iter1, iter2, data): + return cmp(model[iter1][data], model[iter2][data]) + + def set_sort_functions(self): + for column in self.columns.values(): + sort_func = column.sort_func or self.generic_sort_func + self.model_filter.set_sort_func( + column.sort_id, self.stabilize_sort_func(sort_func), column.sort_id + ) + + def create_column_state(self, column, position=None): + if not position: + # Find the position + for index, c in enumerate(self.treeview.get_columns()): + if column.get_title() == c.get_title(): + position = index + break + sort = None + if self.model_filter: + sort_id, order = self.model_filter.get_sort_column_id() + col_title = decode_bytes(column.get_title()) + if self.get_column_name(sort_id) == col_title: + sort = sort_id + + return ListViewColumnState( + column.get_title(), + position, + column.get_width(), + column.get_visible(), + sort, + int(column.get_sort_order()), + ) + + def save_state(self, filename): + """Saves the listview state (column positions and visibility) to + filename.""" + # A list of ListViewColumnStates + state = [] + + # Workaround for all zero widths after removing column on shutdown + if not any(c.get_width() for c in self.treeview.get_columns()): + return + + # Get the list of TreeViewColumns from the TreeView + for counter, column in enumerate(self.treeview.get_columns()): + # Append a new column state to the state list + state.append(self.create_column_state(column, counter)) + + state += self.removed_columns_state + + self.state = state + save_pickled_state_file(filename, state) + + def load_state(self, filename): + """Load the listview state from filename.""" + self.state = load_pickled_state_file(filename) + + def set_treeview(self, treeview_widget): + """Set the treeview widget that this listview uses.""" + self.treeview = treeview_widget + self.treeview.set_model(self.liststore) + return + + def get_column_index(self, name): + """Get the liststore column indices belonging to this column. + Will return a list. + """ + return self.columns[name].column_indices + + def get_column_name(self, index): + """Get the header name for a liststore column index""" + for key, value in self.columns.items(): + if index in value.column_indices: + return key + + def get_state_field_column(self, field): + """Returns the column number for the state field""" + for column in self.columns: + if self.columns[column].status_field is None: + continue + + for f in self.columns[column].status_field: + if field == f: + return self.columns[column].column_indices[ + self.columns[column].status_field.index(f) + ] + + def on_menuitem_toggled(self, widget): + """Callback for the generated column menuitems.""" + # Get the column name from the widget + name = widget.get_child().get_text() + + # Set the column's visibility based on the widgets active state + try: + self.columns[name].column.set_visible(widget.get_active()) + except KeyError: + self.columns[decode_bytes(name)].column.set_visible(widget.get_active()) + return + + def on_treeview_header_right_clicked(self, column, event): + if event.button == 3: + self.menu.popup(None, None, None, None, event.button, event.get_time()) + + def register_checklist_menu(self, menu): + """Register a checklist menu with the listview. It will automatically + attach any new checklist menu it makes to this menu. + """ + self.checklist_menus.append(menu) + + def create_checklist_menu(self): + """Creates a menu used for toggling the display of columns.""" + menu = self.menu = Gtk.Menu() + # Iterate through the column_index list to preserve order + for name in self.column_index: + column = self.columns[name] + # If the column is hidden, then we do not want to show it in the + # menu. + if column.hidden is True: + continue + menuitem = Gtk.CheckMenuItem.new_with_label(column.name) + # If the column is currently visible, make sure it's set active + # (or checked) in the menu. + if column.column.get_visible() is True: + menuitem.set_active(True) + # Connect to the 'toggled' event + menuitem.connect('toggled', self.on_menuitem_toggled) + # Add the new checkmenuitem to the menu + menu.append(menuitem) + + # Attach this new menu to all the checklist_menus + for _menu in self.checklist_menus: + _menu.set_submenu(menu) + _menu.show_all() + return menu + + def create_new_liststore(self): + """Creates a new GtkListStore based on the liststore_columns list""" + # Create a new liststore with added column and move the data from the + # old one to the new one. + new_list = Gtk.ListStore(*tuple(self.liststore_columns)) + + # This function is used in the liststore.foreach method with user_data + # being the new liststore and the columns list + def copy_row(model, path, row, user_data): + new_list, columns = user_data + new_row = new_list.append() + for column in range(len(columns)): + # Get the current value of the column for this row + value = model.get_value(row, column) + # Set the value of this row and column in the new liststore + new_list.set_value(new_row, column, value) + + # Do the actual row copy + if self.liststore is not None: + self.liststore.foreach(copy_row, (new_list, self.columns)) + + self.liststore = new_list + + def update_treeview_column(self, header, add=True): + """Update TreeViewColumn based on ListView column mappings""" + column = self.columns[header] + tree_column = self.columns[header].column + + if column.column_type == 'text': + if add: + tree_column.pack_start(column.renderer, True) + tree_column.set_col_attributes( + column.renderer, add=add, text=column.column_indices[column.text_index] + ) + elif column.column_type == 'bool': + if add: + tree_column.pack_start(column.renderer, True) + tree_column.set_col_attributes( + column.renderer, active=column.column_indices[0] + ) + elif column.column_type == 'func': + if add: + tree_column.pack_start(column.renderer, True) + indice_arg = column.column_indices[0] + if len(column.column_indices) > 1: + indice_arg = tuple(column.column_indices) + tree_column.set_cell_data_func( + column.renderer, column.data_func, indice_arg + ) + elif column.column_type == 'progress': + if add: + tree_column.pack_start(column.renderer, True) + if column.data_func is None: + tree_column.set_col_attributes( + column.renderer, + add=add, + text=column.column_indices[column.text_index], + value=column.column_indices[column.value_index], + ) + else: + tree_column.set_cell_data_func( + column.renderer, column.data_func, tuple(column.column_indices) + ) + elif column.column_type == 'texticon': + if add: + tree_column.pack_start(column.renderer[column.pixbuf_index], False) + tree_column.pack_start(column.renderer[column.text_index], True) + tree_column.set_col_attributes( + column.renderer[column.text_index], + add=add, + text=column.column_indices[column.text_index], + ) + if column.data_func is not None: + tree_column.set_cell_data_func( + column.renderer[column.pixbuf_index], + column.data_func, + column.column_indices[column.pixbuf_index], + ) + return True + + def remove_column(self, header): + """Removes the column with the name 'header' from the listview""" + # Store a copy of this columns state in case it's re-added + state = self.create_column_state(self.columns[header].column) + self.removed_columns_state.append(state) + # Only remove column if column is associated with the treeview. This avoids + # warning on shutdown when GTKUI is closed before plugins try to remove columns + if self.columns[header].column.get_tree_view() is not None: + self.treeview.remove_column(self.columns[header].column) + # Get the column indices + column_indices = self.columns[header].column_indices + # Delete the column + del self.columns[header] + self.column_index.remove(header) + # Shift the column_indices values of those columns affected by the + # removal. Any column_indices > the one removed. + for column in self.columns.values(): + if column.column_indices[0] > column_indices[0]: + # We need to shift this column_indices + for i, index in enumerate(column.column_indices): + column.column_indices[i] = index - len(column_indices) + # Update the associated TreeViewColumn + self.update_treeview_column(column.name, add=False) + + # Remove from the liststore columns list + for index in sorted(column_indices, reverse=True): + del self.liststore_columns[index] + + # Create a new liststore + self.create_new_liststore() + # Create new model for the treeview + self.create_model_filter() + + # Re-create the menu + self.create_checklist_menu() + return + + def add_column( + self, + header, + render, + col_types, + hidden, + position, + status_field, + sortid, + text=0, + value=0, + pixbuf=0, + function=None, + column_type=None, + sort_func=None, + tooltip=None, + default=True, + unique=False, + default_sort=False, + ): + """Adds a column to the ListView""" + # Add the column types to liststore_columns + column_indices = [] + if isinstance(col_types, list): + for col_type in col_types: + self.liststore_columns.append(col_type) + column_indices.append(len(self.liststore_columns) - 1) + else: + self.liststore_columns.append(col_types) + column_indices.append(len(self.liststore_columns) - 1) + + # Add to the index list so we know the order of the visible columns. + if position is not None: + self.column_index.insert(position, header) + else: + self.column_index.append(header) + + # Create a new column object and add it to the list + column = self.TreeviewColumn(header) + self.columns[header] = self.ListViewColumn(header, column_indices) + self.columns[header].column = column + self.columns[header].status_field = status_field + self.columns[header].sort_func = sort_func + self.columns[header].sort_id = column_indices[sortid] + # Store creation details + self.columns[header].column_type = column_type + self.columns[header].renderer = render + self.columns[header].text_index = text + self.columns[header].value_index = value + self.columns[header].pixbuf_index = pixbuf + self.columns[header].data_func = function + + if unique: + self.unique_column_id = column_indices[sortid] + if default_sort: + self.default_sort_column_id = column_indices[sortid] + + # Create a new list with the added column + self.create_new_liststore() + + # Happens only on columns added after the torrent list has been loaded + if self.model_filter: + self.create_model_filter() + + if column_type is None: + return + + self.update_treeview_column(header) + + column.set_sort_column_id(self.columns[header].column_indices[sortid]) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(20) + column.set_reorderable(True) + column.set_visible(not hidden) + column.connect('button-press-event', self.on_treeview_header_right_clicked) + + if tooltip: + column.get_widget().set_tooltip_markup(tooltip) + + # Check for loaded state and apply + column_in_state = False + if self.state is not None: + for column_state in self.state: + if header == decode_bytes(column_state.name): + # We found a loaded state + column_in_state = True + if column_state.width > 0: + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + column.set_fixed_width(column_state.width) + + column.set_visible(column_state.visible) + position = column_state.position + break + + # Set this column to not visible if its not in the state and + # its not supposed to be shown by default + if not column_in_state and not default and not hidden: + column.set_visible(False) + + if position is not None: + self.treeview.insert_column(column, position) + else: + self.treeview.append_column(column) + + # Set hidden in the column + self.columns[header].hidden = hidden + self.columns[header].column = column + # Re-create the menu item because of the new column + self.create_checklist_menu() + + return True + + def add_text_column( + self, + header, + col_type=str, + hidden=False, + position=None, + status_field=None, + sortid=0, + column_type='text', + sort_func=None, + tooltip=None, + default=True, + unique=False, + default_sort=False, + ): + """Add a text column to the listview. Only the header name is required.""" + render = Gtk.CellRendererText() + self.add_column( + header, + render, + col_type, + hidden, + position, + status_field, + sortid, + column_type=column_type, + sort_func=sort_func, + tooltip=tooltip, + default=default, + unique=unique, + default_sort=default_sort, + ) + + return True + + def add_bool_column( + self, + header, + col_type=bool, + hidden=False, + position=None, + status_field=None, + sortid=0, + column_type='bool', + tooltip=None, + default=True, + ): + """Add a bool column to the listview""" + render = Gtk.CellRendererToggle() + self.add_column( + header, + render, + col_type, + hidden, + position, + status_field, + sortid, + column_type=column_type, + tooltip=tooltip, + default=default, + ) + + def add_func_column( + self, + header, + function, + col_types, + sortid=0, + hidden=False, + position=None, + status_field=None, + column_type='func', + sort_func=None, + tooltip=None, + default=True, + ): + """Add a function column to the listview. Need a header name, the + function and the column types.""" + + render = Gtk.CellRendererText() + self.add_column( + header, + render, + col_types, + hidden, + position, + status_field, + sortid, + column_type=column_type, + function=function, + sort_func=sort_func, + tooltip=tooltip, + default=default, + ) + + return True + + def add_progress_column( + self, + header, + col_types=None, + sortid=0, + hidden=False, + position=None, + status_field=None, + function=None, + column_type='progress', + tooltip=None, + sort_func=None, + default=True, + ): + """Add a progress column to the listview.""" + + if col_types is None: + col_types = [float, str] + render = Gtk.CellRendererProgress() + self.add_column( + header, + render, + col_types, + hidden, + position, + status_field, + sortid, + function=function, + column_type=column_type, + value=0, + text=1, + tooltip=tooltip, + sort_func=sort_func, + default=default, + ) + + return True + + def add_texticon_column( + self, + header, + col_types=None, + sortid=1, + hidden=False, + position=None, + status_field=None, + column_type='texticon', + function=None, + sort_func=None, + tooltip=None, + default=True, + default_sort=False, + ): + """Adds a texticon column to the listview.""" + if col_types is None: + col_types = [str, str] + render1 = Gtk.CellRendererPixbuf() + render2 = Gtk.CellRendererText() + + self.add_column( + header, + (render1, render2), + col_types, + hidden, + position, + status_field, + sortid, + column_type=column_type, + function=function, + pixbuf=0, + text=1, + tooltip=tooltip, + sort_func=sort_func, + default=default, + default_sort=default_sort, + ) + + return True + + def on_keypress_search_by_name(self, model, column, key, _iter): + torrent_name_col = self.columns[_('Name')].column_indices[1] + return not model[_iter][torrent_name_col].lower().startswith(key.lower()) + + def restore_columns_order_from_state(self): + if self.state is None: + # No state file exists, so, no reordering can be done + return + columns = self.treeview.get_columns() + + def find_column(header): + for column in columns: + if column.get_title() == header: + return column + + restored_columns = [] + for col_state in self.state: + if col_state.name in restored_columns: + # Duplicate column in state!?!?!? + continue + elif not col_state.visible: + # Column is not visible, no need to reposition + continue + + try: + column_at_position = columns[col_state.position] + except IndexError: + # Ignore extra columns from Plugins in col_state + continue + if col_state.name == column_at_position.get_title(): + # It's in the right position + continue + column = find_column(col_state.name) + if not column: + log.debug( + 'Could not find column matching "%s" on state.', col_state.name + ) + # The cases where I've found that the column could not be found + # is when not using the english locale, ie, the default one, or + # when changing locales between runs. + # On the next load, all should be fine + continue + self.treeview.move_column_after(column, column_at_position) + # Get columns again to keep reordering since positions have changed + columns = self.treeview.get_columns() + restored_columns.append(col_state.name) + self.create_new_liststore() diff --git a/deluge/ui/gtk3/mainwindow.py b/deluge/ui/gtk3/mainwindow.py new file mode 100644 index 0000000..6c871d2 --- /dev/null +++ b/deluge/ui/gtk3/mainwindow.py @@ -0,0 +1,405 @@ +# +# Copyright (C) 2007-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. +# + +import logging +import os.path +from hashlib import sha1 as sha + +import gi +from gi.repository import Gtk +from gi.repository.Gdk import DragAction, WindowState +from twisted.internet import reactor +from twisted.internet.error import ReactorNotRunning + +import deluge.component as component +from deluge.common import decode_bytes, fspeed, is_magnet, is_url, resource_filename +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +from .common import get_clipboard_text, get_deluge_icon, windowing +from .dialogs import PasswordDialog +from .ipcinterface import process_args + +GdkX11 = None +Wnck = None +if windowing('X11'): + try: + from gi.repository import GdkX11 + except ImportError: + pass + + try: + gi.require_version('Wnck', '3.0') + from gi.repository import Wnck + except (ImportError, ValueError): + pass + +log = logging.getLogger(__name__) + + +class _GtkBuilderSignalsHolder: + def connect_signals(self, mapping_or_class): + if isinstance(mapping_or_class, dict): + for name, handler in mapping_or_class.items(): + if hasattr(self, name): + raise RuntimeError( + 'A handler for signal %r has already been registered: %s' + % (name, getattr(self, name)) + ) + setattr(self, name, handler) + else: + for name in dir(mapping_or_class): + if not name.startswith('on_'): + continue + if hasattr(self, name): + raise RuntimeError( + 'A handler for signal %r has already been registered: %s' + % (name, getattr(self, name)) + ) + setattr(self, name, getattr(mapping_or_class, name)) + + +class MainWindow(component.Component): + def __init__(self): + if Wnck: + self.screen = Wnck.Screen.get_default() + component.Component.__init__(self, 'MainWindow', interval=2) + self.config = ConfigManager('gtk3ui.conf') + self.main_builder = Gtk.Builder() + + # Set theme + Gtk.Settings.get_default().set_property( + 'gtk-application-prefer-dark-theme', + self.config['prefer_dark_theme'], + ) + + # Patch this GtkBuilder to avoid connecting signals from elsewhere + # + # Think about splitting up mainwindow gtkbuilder file into the necessary parts + # to avoid GtkBuilder monkey patch. Those parts would then need adding to mainwindow 'by hand'. + self.gtk_builder_signals_holder = _GtkBuilderSignalsHolder() + # FIXME: The deepcopy has been removed: copy.deepcopy(self.main_builder.connect_signals) + self.main_builder.prev_connect_signals = self.main_builder.connect_signals + + def patched_connect_signals(*a, **k): + raise RuntimeError( + 'In order to connect signals to this GtkBuilder instance please use ' + '"component.get(\'MainWindow\').connect_signals()"' + ) + + self.main_builder.connect_signals = patched_connect_signals + + # Get Gtk Builder files Main Window, New release dialog, and Tabs. + ui_filenames = [ + 'main_window.ui', + 'main_window.new_release.ui', + 'main_window.tabs.ui', + 'main_window.tabs.menu_file.ui', + 'main_window.tabs.menu_peer.ui', + ] + for filename in ui_filenames: + self.main_builder.add_from_file( + resource_filename(__package__, os.path.join('glade', filename)) + ) + + self.window = self.main_builder.get_object('main_window') + self.window.set_icon(get_deluge_icon()) + self.tabsbar_pane = self.main_builder.get_object('tabsbar_pane') + self.tabsbar_torrent_info = self.main_builder.get_object('torrent_info') + self.sidebar_pane = self.main_builder.get_object('sidebar_pane') + + # Keep a list of components to pause and resume when changing window state. + self.child_components = ['TorrentView', 'StatusBar', 'TorrentDetails'] + + # Load the window state + self.load_window_state() + + # Keep track of window minimization state so we don't update UI when it is minimized. + self.is_minimized = False + self.restart = False + + self.window.drag_dest_set( + Gtk.DestDefaults.ALL, + [Gtk.TargetEntry.new(target='text/uri-list', flags=0, info=80)], + DragAction.COPY, + ) + + # Connect events + self.window.connect('window-state-event', self.on_window_state_event) + self.window.connect('configure-event', self.on_window_configure_event) + self.window.connect('delete-event', self.on_window_delete_event) + self.window.connect('drag-data-received', self.on_drag_data_received_event) + self.window.connect('notify::is-active', self.on_focus) + self.tabsbar_pane.connect( + 'notify::position', self.on_tabsbar_pane_position_event + ) + self.sidebar_pane.connect( + 'notify::position', self.on_sidebar_pane_position_event + ) + self.window.connect('draw', self.on_expose_event) + + self.config.register_set_function( + 'show_rate_in_title', self._on_set_show_rate_in_title, apply_now=False + ) + + client.register_event_handler( + 'NewVersionAvailableEvent', self.on_newversionavailable_event + ) + + self.previous_clipboard_text = '' + self.first_run = True + + def connect_signals(self, mapping_or_class): + self.gtk_builder_signals_holder.connect_signals(mapping_or_class) + + def first_show(self): + self.main_builder.prev_connect_signals(self.gtk_builder_signals_holder) + self.sidebar_pane.set_position(self.config['sidebar_position']) + self.tabsbar_pane.set_position(self.config['tabsbar_position']) + + if not ( + self.config['start_in_tray'] and self.config['enable_system_tray'] + ) and not self.window.get_property('visible'): + log.debug('Showing window') + self.show() + + while Gtk.events_pending(): + Gtk.main_iteration() + + def show(self): + component.resume(self.child_components) + self.window.show() + + def hide(self): + component.get('TorrentView').save_state() + component.pause(self.child_components) + self.save_position() + self.window.hide() + + def present(self): + def restore(): + # Restore the proper x,y coords for the window prior to showing it + component.resume(self.child_components) + timestamp = self.get_timestamp() + if windowing('X11'): + # Use present with X11 set_user_time since + # present_with_time is inconsistent. + self.window.present() + self.window.get_window().set_user_time(timestamp) + else: + self.window.present_with_time(timestamp) + self.load_window_state() + + if self.config['lock_tray'] and not self.visible(): + dialog = PasswordDialog(_('Enter your password to show Deluge...')) + + def on_dialog_response(response_id): + if response_id == Gtk.ResponseType.OK: + if ( + self.config['tray_password'] + == sha(decode_bytes(dialog.get_password()).encode()).hexdigest() + ): + restore() + + dialog.run().addCallback(on_dialog_response) + else: + restore() + + def get_timestamp(self): + """Returns the timestamp for the windowing server.""" + timestamp = 0 + gdk_window = self.window.get_window() + if GdkX11 and isinstance(gdk_window, GdkX11.X11Window): + timestamp = GdkX11.x11_get_server_time(gdk_window) + return timestamp + + def active(self): + """Returns True if the window is active, False if not.""" + return self.window.is_active() + + def visible(self): + """Returns True if window is visible, False if not.""" + return self.window.get_visible() + + def get_builder(self): + """Returns a reference to the main window GTK builder object.""" + return self.main_builder + + def quit(self, shutdown=False, restart=False): # noqa: A003 python builtin + """Quits the GtkUI application. + + Args: + shutdown (bool): Whether or not to shutdown the daemon as well. + restart (bool): Whether or not to restart the application after closing. + + """ + + def quit_gtkui(): + def stop_gtk_reactor(result=None): + self.restart = restart + try: + reactor.callLater(0, reactor.fireSystemEvent, 'gtkui_close') + except ReactorNotRunning: + log.debug('Attempted to stop the reactor but it is not running...') + + if shutdown: + client.daemon.shutdown().addCallback(stop_gtk_reactor) + elif not client.is_standalone() and client.connected(): + client.disconnect().addCallback(stop_gtk_reactor) + else: + stop_gtk_reactor() + + if self.config['lock_tray'] and not self.visible(): + dialog = PasswordDialog(_('Enter your password to Quit Deluge...')) + + def on_dialog_response(response_id): + if response_id == Gtk.ResponseType.OK: + if ( + self.config['tray_password'] + == sha(decode_bytes(dialog.get_password()).encode()).hexdigest() + ): + quit_gtkui() + + dialog.run().addCallback(on_dialog_response) + else: + quit_gtkui() + + def load_window_state(self): + if ( + self.config['window_x_pos'] == -32000 + or self.config['window_x_pos'] == -32000 + ): + self.config['window_x_pos'] = self.config['window_y_pos'] = 0 + + self.window.move(self.config['window_x_pos'], self.config['window_y_pos']) + self.window.resize(self.config['window_width'], self.config['window_height']) + if self.config['window_maximized']: + self.window.maximize() + + def save_position(self): + self.config['window_maximized'] = self.window.props.is_maximized + if not self.config['window_maximized'] and self.visible(): + ( + self.config['window_x_pos'], + self.config['window_y_pos'], + ) = self.window.get_position() + ( + self.config['window_width'], + self.config['window_height'], + ) = self.window.get_size() + + def on_window_configure_event(self, widget, event): + self.save_position() + + def on_window_state_event(self, widget, event): + if event.changed_mask & WindowState.ICONIFIED: + if event.new_window_state & WindowState.ICONIFIED: + log.debug('MainWindow is minimized..') + component.get('TorrentView').save_state() + component.pause(self.child_components) + self.is_minimized = True + else: + log.debug('MainWindow is not minimized..') + component.resume(self.child_components) + self.is_minimized = False + return False + + def on_window_delete_event(self, widget, event): + if self.config['close_to_tray'] and self.config['enable_system_tray']: + self.hide() + else: + self.quit() + + return True + + def on_tabsbar_pane_position_event(self, obj, param): + self.config['tabsbar_position'] = self.tabsbar_pane.get_position() + + def on_sidebar_pane_position_event(self, obj, param): + self.config['sidebar_position'] = self.sidebar_pane.get_position() + + def on_drag_data_received_event( + self, widget, drag_context, x, y, selection_data, info, timestamp + ): + log.debug('Selection(s) dropped on main window %s', selection_data.get_text()) + if selection_data.get_uris(): + process_args(selection_data.get_uris()) + else: + process_args(selection_data.get_text().split()) + drag_context.finish(True, True, timestamp) + + def on_expose_event(self, widget, event): + component.get('SystemTray').blink(False) + + def on_focus(self, window, param): + if window.props.is_active and not self.first_run and self.config['detect_urls']: + text = get_clipboard_text() + if text == self.previous_clipboard_text: + return + self.previous_clipboard_text = text + if text and ( + (is_url(text) and text.endswith('.torrent')) + or is_magnet(text) + and not component.get('MenuBar').magnet_copied() + ): + component.get('AddTorrentDialog').show() + component.get('AddTorrentDialog').on_button_url_clicked(window) + self.first_run = False + + def stop(self): + self.window.set_title('Deluge') + + def update(self): + # Update the window title + def _on_get_session_status(status): + download_rate = fspeed( + status['payload_download_rate'], precision=0, shortform=True + ) + upload_rate = fspeed( + status['payload_upload_rate'], precision=0, shortform=True + ) + self.window.set_title( + _('D: {download_rate} U: {upload_rate} - Deluge').format( + download_rate=download_rate, upload_rate=upload_rate + ) + ) + + if self.config['show_rate_in_title']: + client.core.get_session_status( + ['payload_download_rate', 'payload_upload_rate'] + ).addCallback(_on_get_session_status) + + def _on_set_show_rate_in_title(self, key, value): + if value: + self.update() + else: + self.window.set_title(_('Deluge')) + + def on_newversionavailable_event(self, new_version): + if self.config['show_new_releases']: + from .new_release_dialog import NewReleaseDialog + + reactor.callLater(5.0, NewReleaseDialog().show, new_version) + + def is_on_active_workspace(self): + """Determines if MainWindow is on the active workspace. + + Returns: + bool: True if on active workspace (or wnck module not available), otherwise False. + + """ + + if Wnck: + self.screen.force_update() + win = Wnck.Window.get(self.window.get_window().get_xid()) + if win: + active_wksp = win.get_screen().get_active_workspace() + if active_wksp: + return win.is_on_workspace(active_wksp) + return False + return True diff --git a/deluge/ui/gtk3/menubar.py b/deluge/ui/gtk3/menubar.py new file mode 100644 index 0000000..9165320 --- /dev/null +++ b/deluge/ui/gtk3/menubar.py @@ -0,0 +1,640 @@ +# +# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> +# +# 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. +# + + +import logging +import os.path + +from gi.repository import Gtk + +import deluge.common +import deluge.component as component +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +from .dialogs import CopyMagnetDialog, ErrorDialog, OtherDialog +from .path_chooser import PathChooser + +log = logging.getLogger(__name__) + +default_main_window_accelmap = { + '<Deluge-MainWindow>/File/Add Torrent': '<Primary>o', + '<Deluge-MainWindow>/File/Create Torrent': '<Primary>n', + '<Deluge-MainWindow>/File/Quit & Shutdown Daemon': '<Primary><Shift>q', + '<Deluge-MainWindow>/File/Quit': '<Primary>q', + '<Deluge-MainWindow>/Edit/Preferences': '<Primary>p', + '<Deluge-MainWindow>/Edit/Connection Manager': '<Primary>m', + '<Deluge-MainWindow>/View/Find ...': '<Primary>f', + '<Deluge-MainWindow>/Help/FAQ': 'F1', +} + + +class MenuBar(component.Component): + def __init__(self): + log.debug('MenuBar init..') + component.Component.__init__(self, 'MenuBar') + self.mainwindow = component.get('MainWindow') + self.main_builder = self.mainwindow.get_builder() + self.config = ConfigManager('gtk3ui.conf') + self._magnet_copied = False + + self.builder = Gtk.Builder() + # Get the torrent menu from the gtk builder file + self.builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'torrent_menu.ui') + ) + ) + # Get the torrent options menu from the gtk builder file + self.builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'torrent_menu.options.ui') + ) + ) + # Get the torrent queue menu from the gtk builder file + self.builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'torrent_menu.queue.ui') + ) + ) + + # Attach queue torrent menu + torrent_queue_menu = self.builder.get_object('queue_torrent_menu') + self.builder.get_object('menuitem_queue').set_submenu(torrent_queue_menu) + # Attach options torrent menu + torrent_options_menu = self.builder.get_object('options_torrent_menu') + self.builder.get_object('menuitem_options').set_submenu(torrent_options_menu) + + self.builder.get_object('download-limit-image').set_from_file( + deluge.common.get_pixmap('downloading16.png') + ) + 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', + ): + submenu = Gtk.Menu() + item = Gtk.MenuItem.new_with_label(_('Set Unlimited')) + item.set_name(menuitem) + item.connect('activate', self.on_menuitem_set_unlimited) + submenu.append(item) + item = Gtk.MenuItem.new_with_label(_('Other...')) + item.set_name(menuitem) + item.connect('activate', self.on_menuitem_set_other) + submenu.append(item) + submenu.show_all() + self.builder.get_object(menuitem).set_submenu(submenu) + + submenu = Gtk.Menu() + item = Gtk.MenuItem.new_with_label(_('On')) + item.connect('activate', self.on_menuitem_set_automanaged_on) + submenu.append(item) + item = Gtk.MenuItem.new_with_label(_('Off')) + item.connect('activate', self.on_menuitem_set_automanaged_off) + submenu.append(item) + submenu.show_all() + self.builder.get_object('menuitem_auto_managed').set_submenu(submenu) + + submenu = Gtk.Menu() + item = Gtk.MenuItem.new_with_label(_('Disable')) + item.connect('activate', self.on_menuitem_set_stop_seed_at_ratio_disable) + submenu.append(item) + item = Gtk.MenuItem.new_with_label(_('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') + + # Attach the torrent_menu to the Torrent file menu + self.menu_torrent.set_submenu(self.torrentmenu) + + # Set keyboard shortcuts + for accel_path, accelerator in default_main_window_accelmap.items(): + accel_key, accel_mods = Gtk.accelerator_parse(accelerator) + Gtk.AccelMap.change_entry(accel_path, accel_key, accel_mods, True) + + # Make sure the view menuitems are showing the correct active state + self.main_builder.get_object('menuitem_toolbar').set_active( + self.config['show_toolbar'] + ) + self.main_builder.get_object('menuitem_sidebar').set_active( + self.config['show_sidebar'] + ) + self.main_builder.get_object('menuitem_statusbar').set_active( + self.config['show_statusbar'] + ) + 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'] + ) + self.main_builder.get_object('sidebar_show_owners').set_active( + self.config['sidebar_show_owners'] + ) + + # Connect main window Signals # + self.mainwindow.connect_signals(self) + + # Connect menubar signals + self.builder.connect_signals(self) + + self.change_sensitivity = ['menuitem_addtorrent'] + + def magnet_copied(self): + """ + lets the caller know whether a magnet was copied internally + + the `mainwindow` checks every time the data in the clipboard, + so it will automatically open the AddTorrentURL dialog in case it + contains a valid link (URL to a torrent or a magnet URI). + + """ + val = self._magnet_copied + self._magnet_copied = False + return val + + def start(self): + for widget in self.change_sensitivity: + self.main_builder.get_object(widget).set_sensitive(True) + + # Only show open_folder menuitem and separator if connected to a localhost daemon. + localhost_items = ['menuitem_open_folder', 'separator4'] + if client.is_localhost(): + for widget in localhost_items: + self.builder.get_object(widget).show() + self.builder.get_object(widget).set_no_show_all(False) + else: + for widget in localhost_items: + self.builder.get_object(widget).hide() + self.builder.get_object(widget).set_no_show_all(True) + + self.main_builder.get_object('separatormenuitem').set_visible( + not self.config['standalone'] + ) + self.main_builder.get_object('menuitem_quitdaemon').set_visible( + not self.config['standalone'] + ) + self.main_builder.get_object('menuitem_connectionmanager').set_visible( + not self.config['standalone'] + ) + + # Show the Torrent menu because we're connected to a host + self.menu_torrent.show() + + 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) + + client.register_event_handler( + 'TorrentStateChangedEvent', self.on_torrentstatechanged_event + ) + client.register_event_handler( + 'TorrentResumedEvent', self.on_torrentresumed_event + ) + client.register_event_handler('SessionPausedEvent', self.on_sessionpaused_event) + client.register_event_handler( + 'SessionResumedEvent', self.on_sessionresumed_event + ) + + def stop(self): + log.debug('MenuBar stopping') + + client.deregister_event_handler( + 'TorrentStateChangedEvent', self.on_torrentstatechanged_event + ) + client.deregister_event_handler( + 'TorrentResumedEvent', self.on_torrentresumed_event + ) + client.deregister_event_handler( + 'SessionPausedEvent', self.on_sessionpaused_event + ) + client.deregister_event_handler( + 'SessionResumedEvent', self.on_sessionresumed_event + ) + + for widget in self.change_sensitivity: + self.main_builder.get_object(widget).set_sensitive(False) + + # Hide the Torrent menu + self.menu_torrent.hide() + + self.main_builder.get_object('separatormenuitem').hide() + self.main_builder.get_object('menuitem_quitdaemon').hide() + + def update_menu(self): + selected = component.get('TorrentView').get_selected_torrents() + if not selected or len(selected) == 0: + # No torrent is selected. Disable the 'Torrents' menu + self.menu_torrent.set_sensitive(False) + return + + self.menu_torrent.set_sensitive(True) + # XXX: Should also update Pause/Resume/Remove menuitems. + # Any better way than duplicating toolbar.py:update_buttons in here? + + def add_torrentmenu_separator(self): + sep = Gtk.SeparatorMenuItem() + self.torrentmenu.append(sep) + sep.show() + return sep + + # Callbacks # + def on_torrentstatechanged_event(self, torrent_id, state): + if state == 'Paused': + self.update_menu() + + def on_torrentresumed_event(self, torrent_id): + self.update_menu() + + def on_sessionpaused_event(self): + self.update_menu() + + def on_sessionresumed_event(self): + self.update_menu() + + # File Menu # + def on_menuitem_addtorrent_activate(self, data=None): + log.debug('on_menuitem_addtorrent_activate') + component.get('AddTorrentDialog').show() + + def on_menuitem_createtorrent_activate(self, data=None): + log.debug('on_menuitem_createtorrent_activate') + from .createtorrentdialog import CreateTorrentDialog + + CreateTorrentDialog().show() + + def on_menuitem_quitdaemon_activate(self, data=None): + log.debug('on_menuitem_quitdaemon_activate') + self.mainwindow.quit(shutdown=True) + + def on_menuitem_quit_activate(self, data=None): + log.debug('on_menuitem_quit_activate') + self.mainwindow.quit() + + # Edit Menu # + def on_menuitem_preferences_activate(self, data=None): + log.debug('on_menuitem_preferences_activate') + component.get('Preferences').show() + + def on_menuitem_connectionmanager_activate(self, data=None): + log.debug('on_menuitem_connectionmanager_activate') + component.get('ConnectionManager').show() + + # Torrent Menu # + def on_menuitem_pause_activate(self, data=None): + log.debug('on_menuitem_pause_activate') + client.core.pause_torrents(component.get('TorrentView').get_selected_torrents()) + + def on_menuitem_resume_activate(self, data=None): + log.debug('on_menuitem_resume_activate') + client.core.resume_torrents( + component.get('TorrentView').get_selected_torrents() + ) + + def on_menuitem_copymagnet_activate(self, data=None): + log.debug('on_menuitem_copymagnet_activate') + torrent_ids = component.get('TorrentView').get_selected_torrents() + if torrent_ids: + + def _on_magnet_uri(magnet_uri): + def update_copied(response_id): + if dialog.copied: + self._magnet_copied = True + + dialog = CopyMagnetDialog(magnet_uri) + dialog.run().addCallback(update_copied) + + client.core.get_magnet_uri(torrent_ids[0]).addCallback(_on_magnet_uri) + + def on_menuitem_updatetracker_activate(self, data=None): + log.debug('on_menuitem_updatetracker_activate') + client.core.force_reannounce( + component.get('TorrentView').get_selected_torrents() + ) + + def on_menuitem_edittrackers_activate(self, data=None): + log.debug('on_menuitem_edittrackers_activate') + from .edittrackersdialog import EditTrackersDialog + + dialog = EditTrackersDialog( + component.get('TorrentView').get_selected_torrent(), self.mainwindow.window + ) + dialog.run() + + def on_menuitem_remove_activate(self, data=None): + log.debug('on_menuitem_remove_activate') + torrent_ids = component.get('TorrentView').get_selected_torrents() + if torrent_ids: + from .removetorrentdialog import RemoveTorrentDialog + + RemoveTorrentDialog(torrent_ids).run() + + def on_menuitem_recheck_activate(self, data=None): + log.debug('on_menuitem_recheck_activate') + client.core.force_recheck(component.get('TorrentView').get_selected_torrents()) + + def on_menuitem_open_folder_activate(self, data=None): + log.debug('on_menuitem_open_folder') + + def _on_torrent_status(status): + timestamp = component.get('MainWindow').get_timestamp() + path = os.path.join( + status['download_location'], status['files'][0]['path'].split('/')[0] + ) + deluge.common.show_file(path, timestamp=timestamp) + + for torrent_id in component.get('TorrentView').get_selected_torrents(): + component.get('SessionProxy').get_torrent_status( + torrent_id, ['download_location', 'files'] + ).addCallback(_on_torrent_status) + + def on_menuitem_move_activate(self, data=None): + log.debug('on_menuitem_move_activate') + component.get('SessionProxy').get_torrent_status( + component.get('TorrentView').get_selected_torrent(), ['download_location'] + ).addCallback(self.show_move_storage_dialog) + + def show_move_storage_dialog(self, status): + log.debug('show_move_storage_dialog') + builder = Gtk.Builder() + builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'move_storage_dialog.ui') + ) + ) + # Keep it referenced: + # https://bugzilla.gnome.org/show_bug.cgi?id=546802 + self.move_storage_dialog = builder.get_object('move_storage_dialog') + self.move_storage_dialog.set_transient_for(self.mainwindow.window) + self.move_storage_dialog_hbox = builder.get_object('hbox_entry') + self.move_storage_path_chooser = PathChooser( + 'move_completed_paths_list', self.move_storage_dialog + ) + self.move_storage_dialog_hbox.add(self.move_storage_path_chooser) + self.move_storage_dialog_hbox.show_all() + self.move_storage_path_chooser.set_text(status['download_location']) + + def on_dialog_response_event(widget, response_id): + def on_core_result(result): + # Delete references + self.move_storage_dialog.hide() + del self.move_storage_dialog + del self.move_storage_dialog_hbox + + if response_id == Gtk.ResponseType.CANCEL: + on_core_result(None) + + if response_id == Gtk.ResponseType.OK: + log.debug( + 'Moving torrents to %s', self.move_storage_path_chooser.get_text() + ) + path = self.move_storage_path_chooser.get_text() + client.core.move_storage( + component.get('TorrentView').get_selected_torrents(), path + ).addCallback(on_core_result) + + self.move_storage_dialog.connect('response', on_dialog_response_event) + self.move_storage_dialog.show() + + def on_menuitem_queue_top_activate(self, value): + log.debug('on_menuitem_queue_top_activate') + client.core.queue_top(component.get('TorrentView').get_selected_torrents()) + + def on_menuitem_queue_up_activate(self, value): + log.debug('on_menuitem_queue_up_activate') + client.core.queue_up(component.get('TorrentView').get_selected_torrents()) + + def on_menuitem_queue_down_activate(self, value): + log.debug('on_menuitem_queue_down_activate') + client.core.queue_down(component.get('TorrentView').get_selected_torrents()) + + def on_menuitem_queue_bottom_activate(self, value): + log.debug('on_menuitem_queue_bottom_activate') + client.core.queue_bottom(component.get('TorrentView').get_selected_torrents()) + + # View Menu # + def on_menuitem_toolbar_toggled(self, value): + log.debug('on_menuitem_toolbar_toggled') + component.get('ToolBar').visible(value.get_active()) + + def on_menuitem_sidebar_toggled(self, value): + log.debug('on_menuitem_sidebar_toggled') + component.get('SideBar').visible(value.get_active()) + + def on_menuitem_statusbar_toggled(self, value): + log.debug('on_menuitem_statusbar_toggled') + component.get('StatusBar').visible(value.get_active()) + + # Help Menu # + def on_menuitem_homepage_activate(self, data=None): + log.debug('on_menuitem_homepage_activate') + deluge.common.open_url_in_browser('http://deluge-torrent.org') + + def on_menuitem_faq_activate(self, data=None): + log.debug('on_menuitem_faq_activate') + deluge.common.open_url_in_browser('http://dev.deluge-torrent.org/wiki/Faq') + + def on_menuitem_community_activate(self, data=None): + log.debug('on_menuitem_community_activate') + deluge.common.open_url_in_browser('http://forum.deluge-torrent.org/') + + def on_menuitem_about_activate(self, data=None): + log.debug('on_menuitem_about_activate') + from .aboutdialog import AboutDialog + + AboutDialog().run() + + def on_menuitem_set_unlimited(self, widget): + log.debug('widget name: %s', widget.get_name()) + funcs = { + 'menuitem_down_speed': 'max_download_speed', + 'menuitem_up_speed': 'max_upload_speed', + 'menuitem_max_connections': 'max_connections', + 'menuitem_upload_slots': 'max_upload_slots', + } + if widget.get_name() in funcs: + torrent_ids = component.get('TorrentView').get_selected_torrents() + client.core.set_torrent_options(torrent_ids, {funcs[widget.get_name()]: -1}) + + def on_menuitem_set_other(self, widget): + log.debug('widget name: %s', widget.get_name()) + 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'], + } + + 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'), + '', + 'network-transmit-receive-symbolic', + ], + 'menuitem_upload_slots': [ + _('Peer Upload Slots'), + _('Set the maximum upload slots'), + '', + 'view-sort-descending-symbolic', + ], + 'menuitem_stop_seed_at_ratio': [ + _('Stop Seed At Ratio'), + 'Stop torrent seeding at ratio', + '', + None, + ], + } + + core_key = status_map[widget.get_name()][0] + core_key_global = status_map[widget.get_name()][1] + + def _on_torrent_status(status): + other_dialog = other_dialog_info[widget.get_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 = 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): + client.core.set_torrent_options( + component.get('TorrentView').get_selected_torrents(), {'auto_managed': True} + ) + + def on_menuitem_set_automanaged_off(self, widget): + client.core.set_torrent_options( + component.get('TorrentView').get_selected_torrents(), + {'auto_managed': 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() + + def on_menuitem_sidebar_trackers_toggled(self, widget): + self.config['sidebar_show_trackers'] = widget.get_active() + component.get('FilterTreeView').update() + + def on_menuitem_sidebar_owners_toggled(self, widget): + self.config['sidebar_show_owners'] = widget.get_active() + component.get('FilterTreeView').update() + + def _on_known_accounts(self, known_accounts): + menuitem_change_owner = self.builder.get_object('menuitem_change_owner') + if len(known_accounts) <= 1: + menuitem_change_owner.set_visible(False) + return + + self.users_menu = Gtk.Menu() + self.users_menu_items = {} + menu_group = None + + for account in known_accounts: + username = account['username'] + item = Gtk.RadioMenuItem.new_with_label(menu_group, username) + menu_group = item.get_group() + item.connect('toggled', self._on_change_owner_toggled, username) + self.users_menu_items[username] = item + self.users_menu.append(item) + + self.users_menu.show_all() + menuitem_change_owner.set_submenu(self.users_menu) + menuitem_change_owner.set_visible(True) + + def _on_known_accounts_fail(self, reason): + self.builder.get_object('menuitem_change_owner').set_visible(False) + + def _on_change_owner_submenu_active(self, widget): + log.debug('_on_change_owner_submenu_active') + selected = component.get('TorrentView').get_selected_torrents() + if len(selected) > 1: + self.users_menu_items[None].set_active(True) + return + + torrent_owner = component.get('TorrentView').get_torrent_status(selected[0])[ + 'owner' + ] + for username, item in self.users_menu_items.items(): + item.set_active(username == torrent_owner) + + 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'] != username: + update_torrents.append(torrent_id) + + if update_torrents: + log.debug('Setting torrent owner "%s" on %s', username, update_torrents) + + def failed_change_owner(failure): + ErrorDialog( + _('Ownership Change Error'), + _('There was an error while trying changing ownership.'), + self.mainwindow.window, + details=failure.value.logable(), + ).run() + + client.core.set_torrent_options( + update_torrents, {'owner': username} + ).addErrback(failed_change_owner) diff --git a/deluge/ui/gtk3/menubar_osx.py b/deluge/ui/gtk3/menubar_osx.py new file mode 100644 index 0000000..7f846dc --- /dev/null +++ b/deluge/ui/gtk3/menubar_osx.py @@ -0,0 +1,68 @@ +# +# Copyright (C) 2007-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. +# +from gi.repository import Gtk + +from deluge.configmanager import ConfigManager + +macos_main_window_accelmap = { + '<Deluge-MainWindow>/File/Add Torrent': '<Meta>o', + '<Deluge-MainWindow>/File/Create Torrent': '<Meta>n', + '<Deluge-MainWindow>/File/Quit & Shutdown Daemon': '<Meta><Shift>q', + '<Deluge-MainWindow>/File/Quit': '<Meta>q', + '<Deluge-MainWindow>/Edit/Preferences': '<Meta>comma', + '<Deluge-MainWindow>/Edit/Connection Manager': '<Meta>m', + '<Deluge-MainWindow>/View/Find ...': '<Meta>f', + '<Deluge-MainWindow>/Help/FAQ': '<Meta>question', +} + + +def menubar_osx(gtkui, osxapp): + # Change key shortcuts + for accel_path, accelerator in macos_main_window_accelmap.items(): + accel_key, accel_mods = Gtk.accelerator_parse(accelerator) + Gtk.AccelMap.change_entry(accel_path, accel_key, accel_mods, True) + + main_builder = gtkui.mainwindow.get_builder() + menubar = main_builder.get_object('menubar') + + config = ConfigManager('gtk3ui.conf') + file_menu = main_builder.get_object('menu_file').get_submenu() + file_items = file_menu.get_children() + quit_all_item = file_items[3] + + for item in range(2, len(file_items)): # remove quits + file_menu.remove(file_items[item]) + + menu_widget = main_builder.get_object('menu_edit') + edit_menu = menu_widget.get_submenu() + edit_items = edit_menu.get_children() + pref_item = edit_items[0] + edit_menu.remove(pref_item) + + conn_item = edit_items[1] + edit_menu.remove(conn_item) + + menubar.remove(menu_widget) + + help_menu = main_builder.get_object('menu_help').get_submenu() + help_items = help_menu.get_children() + about_item = help_items[4] + help_menu.remove(about_item) + help_menu.remove(help_items[3]) # separator + + menubar.hide() + osxapp.set_menu_bar(menubar) + # populate app menu + osxapp.insert_app_menu_item(about_item, 0) + osxapp.insert_app_menu_item(Gtk.SeparatorMenuItem(), 1) + osxapp.insert_app_menu_item(pref_item, 2) + if not config['standalone']: + osxapp.insert_app_menu_item(conn_item, 3) + if quit_all_item.get_visible(): + osxapp.insert_app_menu_item(Gtk.SeparatorMenuItem(), 4) + osxapp.insert_app_menu_item(quit_all_item, 5) diff --git a/deluge/ui/gtk3/new_release_dialog.py b/deluge/ui/gtk3/new_release_dialog.py new file mode 100644 index 0000000..a635bd2 --- /dev/null +++ b/deluge/ui/gtk3/new_release_dialog.py @@ -0,0 +1,70 @@ +# +# Copyright (C) 2008 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. +# + +from gi.repository.Gtk import IconSize + +import deluge.common +import deluge.component as component +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + + +class NewReleaseDialog: + def __init__(self): + pass + + def show(self, available_version): + self.config = ConfigManager('gtk3ui.conf') + main_builder = component.get('MainWindow').get_builder() + self.dialog = main_builder.get_object('new_release_dialog') + # Set the version labels + if deluge.common.windows_check() or deluge.common.osx_check(): + main_builder.get_object('image_new_release').set_from_file( + deluge.common.get_pixmap('deluge16.png') + ) + else: + main_builder.get_object('image_new_release').set_from_icon_name( + 'deluge', IconSize.LARGE_TOOLBAR + ) + main_builder.get_object('label_available_version').set_text(available_version) + main_builder.get_object('label_client_version').set_text( + deluge.common.get_version() + ) + self.chk_not_show_dialog = main_builder.get_object( + 'chk_do_not_show_new_release' + ) + main_builder.get_object('button_goto_downloads').connect( + 'clicked', self._on_button_goto_downloads + ) + main_builder.get_object('button_close_new_release').connect( + 'clicked', self._on_button_close_new_release + ) + + if client.connected(): + + def on_info(version): + main_builder.get_object('label_server_version').set_text(version) + main_builder.get_object('label_server_version').show() + main_builder.get_object('label_server_version_text').show() + + if not client.is_standalone(): + main_builder.get_object('label_client_version_text').set_label( + _('<i>Client Version</i>') + ) + client.daemon.info().addCallback(on_info) + + self.dialog.show() + + def _on_button_goto_downloads(self, widget): + deluge.common.open_url_in_browser('http://deluge-torrent.org') + self.config['show_new_releases'] = not self.chk_not_show_dialog.get_active() + self.dialog.destroy() + + def _on_button_close_new_release(self, widget): + self.config['show_new_releases'] = not self.chk_not_show_dialog.get_active() + self.dialog.destroy() diff --git a/deluge/ui/gtk3/options_tab.py b/deluge/ui/gtk3/options_tab.py new file mode 100644 index 0000000..b0411a8 --- /dev/null +++ b/deluge/ui/gtk3/options_tab.py @@ -0,0 +1,220 @@ +# +# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> +# 2017 Calum Lind <calumlind+deluge@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. +# + +from gi.repository.Gdk import keyval_name + +import deluge.component as component +from deluge.ui.client import client + +from .path_chooser import PathChooser +from .torrentdetails import Tab + + +class OptionsTab(Tab): + def __init__(self): + super().__init__('Options', 'options_tab', 'options_tab_label') + + self.prev_torrent_ids = None + self.prev_status = None + self.inconsistent_keys = [] + + # Create TabWidget items with widget id, get/set func name, status key. + self.add_tab_widget('spin_max_download', 'value', ['max_download_speed']) + self.add_tab_widget('spin_max_upload', 'value', ['max_upload_speed']) + self.add_tab_widget('spin_max_connections', 'value_as_int', ['max_connections']) + self.add_tab_widget( + 'spin_max_upload_slots', 'value_as_int', ['max_upload_slots'] + ) + self.add_tab_widget( + 'chk_prioritize_first_last', 'active', ['prioritize_first_last_pieces'] + ) + self.add_tab_widget( + 'chk_sequential_download', 'active', ['sequential_download'] + ) + self.add_tab_widget('chk_auto_managed', 'active', ['auto_managed']) + self.add_tab_widget('chk_stop_at_ratio', 'active', ['stop_at_ratio']) + self.add_tab_widget('chk_remove_at_ratio', 'active', ['remove_at_ratio']) + self.add_tab_widget('spin_stop_ratio', 'value', ['stop_ratio']) + self.add_tab_widget('chk_move_completed', 'active', ['move_completed']) + self.add_tab_widget('chk_shared', 'active', ['shared']) + self.add_tab_widget('summary_owner', 'text', ['owner']) + self.add_tab_widget('chk_super_seeding', 'active', ['super_seeding']) + + # Connect key press event for spin widgets. + for widget_id in self.tab_widgets: + if widget_id.startswith('spin_'): + self.tab_widgets[widget_id].obj.connect( + 'key-press-event', self.on_key_press_event + ) + + self.button_apply = self.main_builder.get_object('button_apply') + + self.move_completed_path_chooser = PathChooser( + 'move_completed_paths_list', parent=component.get('MainWindow').window + ) + self.move_completed_path_chooser.set_sensitive( + self.tab_widgets['chk_move_completed'].obj.get_active() + ) + self.move_completed_path_chooser.connect( + 'text-changed', self.on_path_chooser_text_changed_event + ) + self.status_keys.append('move_completed_path') + + self.move_completed_hbox = self.main_builder.get_object( + 'hbox_move_completed_path_chooser' + ) + self.move_completed_hbox.add(self.move_completed_path_chooser) + self.move_completed_hbox.show_all() + + component.get('MainWindow').connect_signals(self) + + def start(self): + pass + + def stop(self): + pass + + def clear(self): + self.prev_torrent_ids = None + self.prev_status = None + self.inconsistent_keys = [] + + def update(self): + torrent_ids = component.get('TorrentView').get_selected_torrents() + + # Set True if torrent(s) selected in torrentview, else False. + self._child_widget.set_sensitive(bool(torrent_ids)) + + if torrent_ids: + if torrent_ids != self.prev_torrent_ids: + self.clear() + + component.get('SessionProxy').get_torrents_status( + {'id': torrent_ids}, self.status_keys + ).addCallback(self.parse_torrents_statuses) + + self.prev_torrent_ids = torrent_ids + + def parse_torrents_statuses(self, statuses): + """Finds common status values to all torrrents in statuses. + + Values which differ are replaced with config values. + + + Args: + statuses (dict): A status dict of {torrent_id: {key: value}}. + + Returns: + dict: A single status dict. + + """ + status = {} + if len(statuses) == 1: + # A single torrent so pop torrent status. + status = statuses.popitem()[1] + self.button_apply.set_label('_Apply') + else: + for status_key in self.status_keys: + prev_value = None + for idx, status in enumerate(statuses.values()): + if idx == 0: + prev_value = status[status_key] + continue + elif status[status_key] != prev_value: + self.inconsistent_keys.append(status_key) + break + status[status_key] = prev_value + self.button_apply.set_label(_('_Apply to selected')) + + self.on_get_torrent_status(status) + + def on_get_torrent_status(self, new_status): + # So we don't overwrite the user's unapplied changes we only + # want to update values that have been applied in the core. + if self.prev_status is None: + self.prev_status = dict.fromkeys(new_status, None) + + if new_status != self.prev_status: + for widget in self.tab_widgets.values(): + status_key = widget.status_keys[0] + status_value = new_status[status_key] + if status_value != self.prev_status[status_key]: + set_func = 'set_' + widget.func.replace('_as_int', '') + getattr(widget.obj, set_func)(status_value) + if set_func == 'set_active': + widget.obj.set_inconsistent( + status_key in self.inconsistent_keys + ) + + if ( + new_status['move_completed_path'] + != self.prev_status['move_completed_path'] + ): + text = new_status['move_completed_path'] + self.move_completed_path_chooser.set_text( + text, cursor_end=False, default_text=True + ) + + # Update sensitivity of widgets. + self.tab_widgets['spin_stop_ratio'].obj.set_sensitive( + new_status['stop_at_ratio'] + ) + self.tab_widgets['chk_remove_at_ratio'].obj.set_sensitive( + new_status['stop_at_ratio'] + ) + + # Ensure apply button sensitivity is set False. + self.button_apply.set_sensitive(False) + self.prev_status = new_status + + # === Widget signal handlers === # + + def on_button_apply_clicked(self, button): + options = {} + for widget in self.tab_widgets.values(): + status_key = widget.status_keys[0] + if status_key == 'owner': + continue # A label so read-only + widget_value = getattr(widget.obj, 'get_' + widget.func)() + if widget_value != self.prev_status[status_key] or ( + status_key in self.inconsistent_keys + and not widget.obj.get_inconsistent() + ): + options[status_key] = widget_value + + move_completed_path = self.move_completed_path_chooser.get_text() + if move_completed_path != self.prev_status['move_completed_path']: + options['move_completed_path'] = move_completed_path + + client.core.set_torrent_options(self.prev_torrent_ids, options) + self.button_apply.set_sensitive(False) + + def on_chk_move_completed_toggled(self, widget): + self.move_completed_path_chooser.set_sensitive(widget.get_active()) + self.on_chk_toggled(widget) + + def on_chk_stop_at_ratio_toggled(self, widget): + self.tab_widgets['spin_stop_ratio'].obj.set_sensitive(widget.get_active()) + self.tab_widgets['chk_remove_at_ratio'].obj.set_sensitive(widget.get_active()) + self.on_chk_toggled(widget) + + def on_chk_toggled(self, widget): + widget.set_inconsistent(False) + self.button_apply.set_sensitive(True) + + def on_spin_value_changed(self, widget): + self.button_apply.set_sensitive(True) + + def on_key_press_event(self, widget, event): + keyname = keyval_name(event.keyval).lstrip('KP_').lower() + if keyname.isdigit() or keyname in ['period', 'minus', 'delete', 'backspace']: + self.button_apply.set_sensitive(True) + + def on_path_chooser_text_changed_event(self, widget, path): + self.button_apply.set_sensitive(True) diff --git a/deluge/ui/gtk3/path_chooser.py b/deluge/ui/gtk3/path_chooser.py new file mode 100644 index 0000000..8058196 --- /dev/null +++ b/deluge/ui/gtk3/path_chooser.py @@ -0,0 +1,198 @@ +# +# Copyright (C) 2013 Bro <bro.development@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. +# + +import logging + +import deluge.component as component +from deluge.ui.client import client + +from .path_combo_chooser import PathChooserComboBox + +log = logging.getLogger(__name__) + + +def singleton(cls): + instances = {} + + def getinstance(): + if cls not in instances: + instances[cls] = cls() + return instances[cls] + + return getinstance + + +@singleton +class PathChoosersHandler(component.Component): + def __init__(self, paths_config_key=None): + # self.chooser_name = "PathChooser_%d" % (len(PathChooser.path_choosers) +1) + component.Component.__init__(self, 'PathChoosersHandler') + self.path_choosers = [] + self.paths_list_keys = [] + self.config_properties = {} + self.started = False + self.config_keys_to_funcs_mapping = { + 'path_chooser_show_chooser_button_on_localhost': 'filechooser_button_visible', + 'path_chooser_show_path_entry': 'path_entry_visible', + 'path_chooser_auto_complete_enabled': 'auto_complete_enabled', + 'path_chooser_show_folder_name': 'show_folder_name_on_button', + 'path_chooser_accelerator_string': 'accelerator_string', + 'path_chooser_show_hidden_files': 'show_hidden_files', + 'path_chooser_max_popup_rows': 'max_popup_rows', + } + + def start(self): + self.started = True + self.update_config_from_core() + + def stop(self): + self.started = False + + def update_config_from_core(self): + def _on_config_values(config): + self.config_properties.update(config) + for chooser in self.path_choosers: + chooser.set_config(config) + + keys = list(self.config_keys_to_funcs_mapping) + keys += self.paths_list_keys + client.core.get_config_values(keys).addCallback(_on_config_values) + + def register_chooser(self, chooser): + chooser.config_key_funcs = {} + for key in self.config_keys_to_funcs_mapping: + chooser.config_key_funcs[key] = [None, None] + chooser.config_key_funcs[key][0] = getattr( + chooser, 'get_%s' % self.config_keys_to_funcs_mapping[key] + ) + chooser.config_key_funcs[key][1] = getattr( + chooser, 'set_%s' % self.config_keys_to_funcs_mapping[key] + ) + + self.path_choosers.append(chooser) + if chooser.paths_config_key not in self.paths_list_keys: + self.paths_list_keys.append(chooser.paths_config_key) + if self.started: + self.update_config_from_core() + else: + chooser.set_config(self.config_properties) + + def set_value_for_path_choosers(self, value, key): + for chooser in self.path_choosers: + chooser.config_key_funcs[key][1](value) + + # Save to core + if key != 'path_chooser_max_popup_rows': + client.core.set_config({key: value}) + else: + # Since the max rows value can be changed fast with a spinbutton, we + # delay saving to core until the values hasn't been changed in 1 second. + self.max_rows_value_set = value + + def update(value_): + # The value hasn't been changed in one second, so save to core + if self.max_rows_value_set == value_: + client.core.set_config({'path_chooser_max_popup_rows': value}) + + from twisted.internet import reactor + + reactor.callLater(1, update, value) + + def on_list_values_changed(self, values, key, caller): + # Save to core + config = {key: values} + client.core.set_config(config) + # Set the values on all path choosers with that key + for chooser in self.path_choosers: + # Found chooser with values from 'key' + if chooser.paths_config_key == key: + chooser.set_values(values) + + def get_config_keys(self): + keys = list(self.config_keys_to_funcs_mapping) + keys += self.paths_list_keys + return keys + + +class PathChooser(PathChooserComboBox): + def __init__(self, paths_config_key=None, parent=None): + self.paths_config_key = paths_config_key + super().__init__(parent=parent) + self.chooser_handler = PathChoosersHandler() + self.chooser_handler.register_chooser(self) + self.set_auto_completer_func(self.on_completion) + self.connect('list-values-changed', self.on_list_values_changed_event) + self.connect( + 'auto-complete-enabled-toggled', self.on_auto_complete_enabled_toggled + ) + self.connect('show-filechooser-toggled', self.on_show_filechooser_toggled) + self.connect( + 'show-folder-name-on-button', self.on_show_folder_on_button_toggled + ) + self.connect('show-path-entry-toggled', self.on_show_path_entry_toggled) + self.connect('accelerator-set', self.on_accelerator_set) + self.connect('max-rows-changed', self.on_max_rows_changed) + self.connect('show-hidden-files-toggled', self.on_show_hidden_files_toggled) + + def on_auto_complete_enabled_toggled(self, widget, value): + self.chooser_handler.set_value_for_path_choosers( + value, 'path_chooser_auto_complete_enabled' + ) + + def on_show_filechooser_toggled(self, widget, value): + self.chooser_handler.set_value_for_path_choosers( + value, 'path_chooser_show_chooser_button_on_localhost' + ) + + def on_show_folder_on_button_toggled(self, widget, value): + self.chooser_handler.set_value_for_path_choosers( + value, 'path_chooser_show_folder_name' + ) + + def on_show_path_entry_toggled(self, widget, value): + self.chooser_handler.set_value_for_path_choosers( + value, 'path_chooser_show_path_entry' + ) + + def on_accelerator_set(self, widget, value): + self.chooser_handler.set_value_for_path_choosers( + value, 'path_chooser_accelerator_string' + ) + + def on_show_hidden_files_toggled(self, widget, value): + self.chooser_handler.set_value_for_path_choosers( + value, 'path_chooser_show_hidden_files' + ) + + def on_max_rows_changed(self, widget, value): + self.chooser_handler.set_value_for_path_choosers( + value, 'path_chooser_max_popup_rows' + ) + + def on_list_values_changed_event(self, widget, values): + self.chooser_handler.on_list_values_changed(values, self.paths_config_key, self) + + def set_config(self, config): + self.config = config + for key in self.config_key_funcs: + if key in config: + try: + self.config_key_funcs[key][1](config[key]) + except TypeError as ex: + log.warning('TypeError: %s', ex) + + # Set the saved paths + if self.paths_config_key and self.paths_config_key in config: + self.set_values(config[self.paths_config_key]) + + def on_completion(self, args): + def on_paths_cb(args): + self.complete(args) + + d = client.core.get_completion_paths(args) + d.addCallback(on_paths_cb) diff --git a/deluge/ui/gtk3/path_combo_chooser.py b/deluge/ui/gtk3/path_combo_chooser.py new file mode 100755 index 0000000..aeb4c7a --- /dev/null +++ b/deluge/ui/gtk3/path_combo_chooser.py @@ -0,0 +1,1730 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Bro <bro.development@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. +# + +import os +import warnings + +from gi.repository import Gdk, GObject, Gtk +from gi.repository.GObject import SignalFlags + +from deluge.common import resource_filename +from deluge.path_chooser_common import get_completion_paths + +# Filter the pygobject signal warning: +# g_value_get_int: assertion 'G_VALUE_HOLDS_INT (value)' failed. +# See: https://gitlab.gnome.org/GNOME/pygobject/issues/12 +warnings.filterwarnings('ignore', '.*g_value_get_int.*G_VALUE_HOLDS_INT.*', Warning) + + +def is_ascii_value(keyval, ascii_key): + try: + # Set show/hide hidden files + if chr(keyval) == ascii_key: + return True + except ValueError: + # Not in ascii range + pass + return False + + +def key_is_up(keyval): + return keyval == Gdk.KEY_Up or keyval == Gdk.KEY_KP_Up + + +def key_is_down(keyval): + return keyval == Gdk.KEY_Down or keyval == Gdk.KEY_KP_Down + + +def key_is_up_or_down(keyval): + return key_is_up(keyval) or key_is_down(keyval) + + +def key_is_pgup_or_pgdown(keyval): + return keyval == Gdk.KEY_Page_Down or keyval == Gdk.KEY_Page_Up + + +def key_is_enter(keyval): + return keyval == Gdk.KEY_Return or keyval == Gdk.KEY_KP_Enter + + +def path_without_trailing_path_sep(path): + while path.endswith('/') or path.endswith('\\'): + if path == '/': + return path + path = path[0:-1] + return path + + +class ValueList: + paths_without_trailing_path_sep = False + + def get_values_count(self): + return len(self.tree_store) + + def get_values(self): + """ + Returns the values in the list. + """ + values = [] + for row in self.tree_store: + values.append(row[0]) + return values + + def add_values( + self, paths, append=True, scroll_to_row=False, clear=False, emit_signal=False + ): + """ + Add paths to the liststore + + :param paths: the paths to add + :type paths: list + :param append: if the values should be appended or inserted + :type append: boolean + :param scroll_to_row: if the treeview should scroll to the new row + :type scroll_to_row: boolean + + """ + if clear: + self.tree_store.clear() + + for path in paths: + if self.paths_without_trailing_path_sep: + path = path_without_trailing_path_sep(path) + if append: + tree_iter = self.tree_store.append([path]) + else: + tree_iter = self.tree_store.insert(0, [path]) + + if scroll_to_row: + self.treeview.grab_focus() + tree_path = self.tree_store.get_path(tree_iter) + # Scroll to path + self.handle_list_scroll(path=tree_path) + + if emit_signal: + self.emit('list-value-added', paths) + self.emit('list-values-changed', self.get_values()) + + def set_values(self, paths, scroll_to_row=False, preserve_selection=True): + """ + Add paths to the liststore + + :param paths: the paths to add + :type paths: list + :param scroll_to_row: if the treeview should scroll to the new row + :type scroll_to_row: boolean + + """ + if not (isinstance(paths, list) or isinstance(paths, tuple)): + return + sel = None + if preserve_selection: + sel = self.get_selection_path() + self.add_values(paths, scroll_to_row=scroll_to_row, clear=True) + if sel: + self.treeview.get_selection().select_path(sel) + + def get_selection_path(self): + """Returns the (first) selected path from a treeview""" + tree_selection = self.treeview.get_selection() + model, tree_paths = tree_selection.get_selected_rows() + if len(tree_paths) > 0: + return tree_paths[0] + return None + + def get_selected_value(self): + path = self.get_selection_path() + if path: + return self.tree_store[path][0] + return None + + def remove_selected_path(self): + path = self.get_selection_path() + if path: + path_value = self.tree_store[path][0] + del self.tree_store[path] + index = path[0] + # The last row was deleted + if index == len(self.tree_store): + index -= 1 + if index >= 0: + path = (index,) + self.treeview.set_cursor(path) + self.set_path_selected(path) + self.emit('list-value-removed', path_value) + self.emit('list-values-changed', self.get_values()) + + def set_selected_value(self, value, select_first=False): + """ + Select the row of the list with value + + :param value: the value to be selected + :type value: str + :param select_first: if the first item should be selected if the value if not found. + :type select_first: boolean + + """ + for i, row in enumerate(self.tree_store): + if row[0] == value: + self.treeview.set_cursor(i) + return + # The value was not found + if select_first: + self.treeview.set_cursor((0,)) + else: + self.treeview.get_selection().unselect_all() + + def set_path_selected(self, path): + self.treeview.get_selection().select_path(path) + + def on_value_list_treeview_key_press_event(self, widget, event): + """ + Mimics Combobox behavior + + Escape or Alt+Up: Close + Enter or Return : Select + """ + keyval = event.keyval + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + alt_up = (state == Gdk.ModifierType.MOD1_MASK) and key_is_up(keyval) + if keyval == Gdk.KEY_Escape or alt_up: + self.popdown() + return True + # Set entry value to the selected row + elif key_is_enter(keyval): + path = self.get_selection_path() + if path: + self.set_entry_value(path, popdown=True) + return True + return False + + def on_treeview_mouse_button_press_event(self, treeview, event, double_click=False): + """ + When left clicking twice, the row value is set for the text entry + and the popup is closed. + + """ + # This is left click + if event.button != 3: + # Double clicked a row, set this as the entry value + # and close the popup + if (double_click and event.type == Gdk.EventType._2BUTTON_PRESS) or ( + not double_click and event.type == Gdk.EventType.BUTTON_PRESS + ): + path = self.get_selection_path() + if path: + self.set_entry_value(path, popdown=True) + return True + return False + + def handle_list_scroll( + self, _next=None, path=None, set_entry=False, swap=False, scroll_window=False + ): + """ + Handles changes to the row selection. + + :param _next: the direction to change selection. True means down and False means up. + None means no change. + :type _next: boolean/None + :param path: the current path. If None, the currently selected path is used. + :type path: tuple + :param set_entry: if the new value should be set in the text entry. + :type set_entry: boolean + :param swap: if the old and new value should be swapped + :type swap: boolean + + """ + if scroll_window: + adjustment = self.completion_scrolled_window.get_vadjustment() + + visible_rows_height = self.get_values_count() + if visible_rows_height > self.max_visible_rows: + visible_rows_height = self.max_visible_rows + + visible_rows_height *= self.row_height + value = adjustment.get_value() + + # Max adjustment value + max_value = adjustment.get_upper() - visible_rows_height + # Set adjustment increment to 3 times the row height + adjustment.set_step_increment(self.row_height * 3) + + if _next: + # If number of values is less than max rows, no scroll + if self.get_values_count() < self.max_visible_rows: + return + value += adjustment.get_step_increment() + if value > max_value: + value = max_value + else: + value -= adjustment.get_step_increment() + if value < 0: + value = 0 + adjustment.set_value(value) + return + + if path is None: + path = self.get_selection_path() + if not path: + # These options require a selected path + if set_entry or swap: + return + # This is a regular scroll, not setting value in entry or swapping rows, + # so we find a path value anyways + path = (0,) + cursor = self.treeview.get_cursor() + if cursor is not None and cursor[0] is not None: + path = cursor[0] + else: + # Since cursor is none, we won't advance the index + _next = None + + # If _next is None, we won't change the selection + if _next is not None: + # We move the selection either one up or down. + # If we reach end of list, we wrap + index = path[0] if path else 0 + index = index + 1 if _next else index - 1 + if index >= len(self.tree_store): + index = 0 + elif index < 0: + index = len(self.tree_store) - 1 + + # We have the index for the new path + new_path = index + if swap: + p1 = self.tree_store[path][0] + p2 = self.tree_store[new_path][0] + self.tree_store.swap( + self.tree_store.get_iter(path), self.tree_store.get_iter(new_path) + ) + self.emit('list-values-reordered', [p1, p2]) + self.emit('list-values-changed', self.get_values()) + path = new_path + + self.treeview.set_cursor(path) + self.treeview.get_selection().select_path(path) + if set_entry: + self.set_entry_value(path) + + +class StoredValuesList(ValueList): + def __init__(self): + self.tree_store = self.builder.get_object('stored_values_tree_store') + self.tree_column = self.builder.get_object('stored_values_treeview_column') + self.rendererText = self.builder.get_object('stored_values_cellrenderertext') + self.paths_without_trailing_path_sep = False + + # Add signal handlers + self.signal_handlers[ + 'on_stored_values_treeview_mouse_button_press_event' + ] = self.on_treeview_mouse_button_press_event + + self.signal_handlers[ + 'on_stored_values_treeview_key_press_event' + ] = self.on_stored_values_treeview_key_press_event + self.signal_handlers[ + 'on_stored_values_treeview_key_release_event' + ] = self.on_stored_values_treeview_key_release_event + + self.signal_handlers[ + 'on_cellrenderertext_edited' + ] = self.on_cellrenderertext_edited + + def on_cellrenderertext_edited(self, cellrenderertext, path, new_text): + """ + Callback on the 'edited' signal. + + Sets the new text in the path and disables editing on the renderer. + """ + new_text = path_without_trailing_path_sep(new_text) + self.tree_store[path][0] = new_text + self.rendererText.set_property('editable', False) + + def on_edit_path(self, path, column): + """ + Starts editing on the provided path + + :param path: the paths to edit + :type path: tuple + :param column: the column to edit + :type column: Gtk.TreeViewColumn + + """ + self.rendererText.set_property('editable', True) + self.treeview.grab_focus() + self.treeview.set_cursor(path, column=column, start_editing=True) + + def on_treeview_mouse_button_press_event(self, treeview, event): + """ + Shows popup on selected row when right clicking + When left clicking twice, the row value is set for the text entry + and the popup is closed. + + """ + # This is left click + if event.button != 3: + super().on_treeview_mouse_button_press_event( + treeview, event, double_click=True + ) + return False + + # This is right click, create popup menu for this row + x = int(event.x) + y = int(event.y) + time = event.time + pthinfo = treeview.get_path_at_pos(x, y) + if pthinfo is not None: + path, col, cellx, celly = pthinfo + treeview.grab_focus() + treeview.set_cursor(path, col, 0) + + self.path_list_popup = Gtk.Menu() + menuitem_edit = Gtk.MenuItem.new_with_label(_('Edit path')) + self.path_list_popup.append(menuitem_edit) + menuitem_remove = Gtk.MenuItem.new_with_label(_('Remove path')) + self.path_list_popup.append(menuitem_remove) + + def on_edit_clicked(widget, path): + self.on_edit_path(path, self.tree_column) + + def on_remove_clicked(widget, path): + self.remove_selected_path() + + menuitem_edit.connect('activate', on_edit_clicked, path) + menuitem_remove.connect('activate', on_remove_clicked, path) + self.path_list_popup.popup(None, None, None, path, event.button, time) + self.path_list_popup.show_all() + + def remove_selected_path(self): + ValueList.remove_selected_path(self) + # Resize popup + PathChooserPopup.popup(self) + + def on_stored_values_treeview_key_press_event(self, widget, event): + super().on_value_list_treeview_key_press_event(widget, event) + # Prevent the default event handler to move the cursor in the list + if key_is_up_or_down(event.keyval): + return True + + def on_stored_values_treeview_key_release_event(self, widget, event): + """ + Mimics Combobox behavior + + Escape or Alt+Up: Close + Enter or Return : Select + + """ + keyval = event.keyval + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + + # Edit selected row + if keyval in [Gdk.KEY_Left, Gdk.KEY_Right, Gdk.KEY_space]: + path = self.get_selection_path() + if path: + self.on_edit_path(path, self.tree_column) + elif key_is_up_or_down(keyval): + # Swap the row value + if event.get_state() & Gdk.ModifierType.CONTROL_MASK: + self.handle_list_scroll(_next=key_is_down(keyval), swap=True) + else: + self.handle_list_scroll(_next=key_is_down(keyval)) + elif key_is_pgup_or_pgdown(event.keyval): + # The cursor has been changed by the default key-press-event handler + # so set the path of the cursor selected + self.set_path_selected(self.treeview.get_cursor()[0]) + elif ctrl: + # Handle key bindings for manipulating the list + # Remove the selected entry + if is_ascii_value(keyval, 'r'): + self.remove_selected_path() + return True + # Add current value to saved list + elif is_ascii_value(keyval, 's'): + super( + PathChooserComboBox, self + ).add_current_value_to_saved_list() # pylint: disable=bad-super-call + return True + # Edit selected value + elif is_ascii_value(keyval, 'e'): + self.edit_selected_path() + return True + + +class CompletionList(ValueList): + def __init__(self): + self.tree_store = self.builder.get_object('completion_tree_store') + self.tree_column = self.builder.get_object('completion_treeview_column') + self.rendererText = self.builder.get_object('completion_cellrenderertext') + self.completion_scrolled_window = self.builder.get_object( + 'completion_scrolled_window' + ) + self.signal_handlers[ + 'on_completion_treeview_key_press_event' + ] = self.on_completion_treeview_key_press_event + self.signal_handlers[ + 'on_completion_treeview_motion_notify_event' + ] = self.on_completion_treeview_motion_notify_event + + # Add super class signal handler + self.signal_handlers[ + 'on_completion_treeview_mouse_button_press_event' + ] = super().on_treeview_mouse_button_press_event + + def reduce_values(self, prefix): + """ + Reduce the values in the liststore to those starting with the prefix. + + :param prefix: the prefix to be matched + :type paths: string + + """ + values = self.get_values() + matching_values = [] + for v in values: + if v.startswith(prefix): + matching_values.append(v) + self.add_values(matching_values, clear=True) + + def on_completion_treeview_key_press_event(self, widget, event): + ret = super().on_value_list_treeview_key_press_event(widget, event) + if ret: + return ret + keyval = event.keyval + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + if key_is_up_or_down(keyval): + self.handle_list_scroll(_next=key_is_down(keyval)) + return True + elif ctrl: + # Set show/hide hidden files + if is_ascii_value(keyval, 'h'): + self.path_entry.set_show_hidden_files( + not self.path_entry.get_show_hidden_files(), do_completion=True + ) + return True + + def on_completion_treeview_motion_notify_event(self, widget, event): + if event.is_hint: + x, y, state = event.window.get_pointer() + else: + x = event.x + y = event.y + + path = self.treeview.get_path_at_pos(int(x), int(y)) + if path: + self.handle_list_scroll(path=path[0], _next=None) + + +class PathChooserPopup: + """This creates the popop window for the ComboEntry.""" + + def __init__(self, min_visible_rows, max_visible_rows, popup_alignment_widget): + self.min_visible_rows = min_visible_rows + # Maximum number of rows to display without scrolling + self.set_max_popup_rows(max_visible_rows) + self.popup_window.realize() + self.alignment_widget = popup_alignment_widget + # If set, the height of this widget is the minimum height + self.popup_buttonbox = None + + def popup(self): + """Make the popup visible.""" + # Entry is not yet visible + if not self.path_entry.get_realized(): + return + self.set_window_position_and_size() + + def popdown(self): + if not self.is_popped_up(): + return + if not self.path_entry.get_realized(): + return + self.popup_window.grab_remove() + self.popup_window.hide() + + def is_popped_up(self): + """Check if window is popped up. + + Returns: + bool: True if popped up, False otherwise. + + """ + return self.popup_window.get_mapped() + + def set_window_position_and_size(self): + if len(self.tree_store) < self.min_visible_rows: + return False + x, y, width, height = self.get_position() + self.popup_window.set_size_request(width, height) + self.popup_window.resize(width, height) + self.popup_window.move(x, y) + self.popup_window.show_all() + + def get_position(self): + """ + Returns the size of the popup window and the coordinates on the screen. + + """ + + # Necessary for the first call, to make treeview.size_request give sensible values + # self.popup_window.realize() + self.treeview.realize() + + # We start with the coordinates of the parent window + z, x, y = self.path_entry.get_window().get_origin() + + # Add the position of the alignment_widget relative to the parent window. + x += self.alignment_widget.get_allocation().x + y += self.alignment_widget.get_allocation().y + + height_extra = 8 + buttonbox_width = 0 + height = self.popup_window.get_preferred_height()[1] + width = self.popup_window.get_preferred_width()[1] + + if self.popup_buttonbox: + buttonbox_height = max( + self.popup_buttonbox.get_preferred_height()[1], + self.popup_buttonbox.get_allocation().height, + ) + buttonbox_width = max( + self.popup_buttonbox.get_preferred_width()[1], + self.popup_buttonbox.get_allocation().width, + ) + treeview_width = self.treeview.get_preferred_width()[1] + # After removing an element from the tree store, self.treeview.get_preferred_width()[0] + # returns -1 for some reason, so the requested width cannot be used until the treeview + # has been displayed once. + if treeview_width != -1: + width = treeview_width + buttonbox_width + # The list is empty, so ignore initial popup width request + # Will be set to the minimum width next + elif len(self.tree_store) == 0: + width = 0 + + if width < self.alignment_widget.get_allocation().width: + width = self.alignment_widget.get_allocation().width + + # 10 is extra spacing + content_width = self.treeview.get_preferred_width()[1] + buttonbox_width + 10 + + # Adjust height according to number of list items + if len(self.tree_store) > 0 and self.max_visible_rows > 0: + # The height for one row in the list + self.row_height = self.treeview.get_preferred_height()[1] / len( + self.tree_store + ) + # Set height to number of rows + height = len(self.tree_store) * self.row_height + height_extra + # Adjust the height according to the max number of rows + max_height = self.row_height * self.max_visible_rows + # Restrict height to max_visible_rows + if max_height + height_extra < height: + height = max_height + height += height_extra + # Increase width because of vertical scrollbar + content_width += 15 + + if self.popup_buttonbox: + # Minimum height is the height of the button box + if height < buttonbox_height + height_extra: + height = buttonbox_height + height_extra + + if content_width > width: + width = content_width + + screen = self.path_entry.get_screen() + monitor_num = screen.get_monitor_at_window(self.path_entry.get_window()) + monitor = screen.get_monitor_geometry(monitor_num) + + if x < monitor.x: + x = monitor.x + elif x + width > monitor.x + monitor.width: + x = monitor.x + monitor.width - width + + # Set the position + if ( + y + self.path_entry.get_allocation().height + height + <= monitor.y + monitor.height + ): + y += self.path_entry.get_allocation().height + # Not enough space downwards on the screen + elif y - height >= monitor.y: + y -= height + elif ( + monitor.y + monitor.height - (y + self.path_entry.get_allocation().height) + > y - monitor.y + ): + y += self.path_entry.get_allocation().height + height = monitor.y + monitor.height - y + else: + height = y - monitor.y + y = monitor.y + + return x, y, width, height + + def popup_grab_window(self): + activate_time = 0 + if ( + Gdk.pointer_grab( + self.popup_window.get_window(), + True, + ( + Gdk.EventMask.BUTTON_PRESS_MASK + | Gdk.EventMask.BUTTON_RELEASE_MASK + | Gdk.EventMask.POINTER_MOTION_MASK + ), + None, + None, + activate_time, + ) + == 0 + ): + if ( + Gdk.keyboard_grab(self.popup_window.get_window(), True, activate_time) + == 0 + ): + return True + else: + self.popup_window.get_window().get_display().pointer_ungrab( + activate_time + ) + return False + return False + + def set_entry_value(self, path, popdown=False): + """ + + Sets the text of the entry to the value in path + """ + self.path_entry.set_text( + self.tree_store[path][0], set_file_chooser_folder=True, trigger_event=True + ) + if popdown: + self.popdown() + + def set_max_popup_rows(self, rows): + try: + int(rows) + except Exception: + self.max_visible_rows = 20 + return + self.max_visible_rows = rows + + def get_max_popup_rows(self): + return self.max_visible_rows + + ################# + # Callbacks + ################# + + def on_popup_window_button_press_event(self, window, event): + # If we're clicking outside of the window close the popup + allocation = self.popup_window.get_allocation() + + if (event.x < allocation.x or event.x > allocation.width) or ( + event.y < allocation.y or event.y > allocation.height + ): + self.popdown() + + +class StoredValuesPopup(StoredValuesList, PathChooserPopup): + """ + + The stored values popup + + """ + + def __init__(self, builder, path_entry, max_visible_rows, popup_alignment_widget): + self.builder = builder + self.treeview = self.builder.get_object('stored_values_treeview') + self.popup_window = self.builder.get_object('stored_values_popup_window') + self.button_default = self.builder.get_object('button_default') + self.path_entry = path_entry + self.text_entry = path_entry.text_entry + + self.signal_handlers = {} + PathChooserPopup.__init__(self, 0, max_visible_rows, popup_alignment_widget) + StoredValuesList.__init__(self) + + self.popup_buttonbox = self.builder.get_object('buttonbox') + + # Add signal handlers + self.signal_handlers[ + 'on_buttonbox_key_press_event' + ] = self.on_buttonbox_key_press_event + self.signal_handlers[ + 'on_stored_values_treeview_scroll_event' + ] = self.on_scroll_event + self.signal_handlers[ + 'on_button_toggle_dropdown_scroll_event' + ] = self.on_scroll_event + self.signal_handlers['on_entry_text_scroll_event'] = self.on_scroll_event + self.signal_handlers[ + 'on_stored_values_popup_window_focus_out_event' + ] = self.on_stored_values_popup_window_focus_out_event + # For when clicking outside the popup + self.signal_handlers[ + 'on_stored_values_popup_window_button_press_event' + ] = self.on_popup_window_button_press_event + + # Buttons for manipulating the list + self.signal_handlers['on_button_add_clicked'] = self.on_button_add_clicked + self.signal_handlers['on_button_edit_clicked'] = self.on_button_edit_clicked + self.signal_handlers['on_button_remove_clicked'] = self.on_button_remove_clicked + self.signal_handlers['on_button_up_clicked'] = self.on_button_up_clicked + self.signal_handlers['on_button_down_clicked'] = self.on_button_down_clicked + self.signal_handlers[ + 'on_button_default_clicked' + ] = self.on_button_default_clicked + self.signal_handlers[ + 'on_button_properties_clicked' + ] = self.path_entry._on_button_properties_clicked + + def popup(self): + """ + Makes the popup visible. + + """ + # Calling super popup + PathChooserPopup.popup(self) + self.popup_window.grab_focus() + + if not self.treeview.has_focus(): + self.treeview.grab_focus() + if not self.popup_grab_window(): + self.popup_window.hide() + return + + self.popup_window.grab_add() + # Set value selected if it exists + self.set_selected_value( + path_without_trailing_path_sep(self.path_entry.get_text()) + ) + + ################# + # Callbacks + ################# + + def on_stored_values_popup_window_focus_out_event(self, entry, event): + """ + Popup sometimes loses the focus to the text entry, e.g. when right click + shows a popup menu on a row. This regains the focus. + """ + self.popup_grab_window() + return True + + def on_scroll_event(self, widget, event): + """ + Handles scroll events from text entry, toggle button and treeview + + """ + + swap = event.get_state() & Gdk.ModifierType.CONTROL_MASK + scroll_window = event.get_state() & Gdk.ModifierType.SHIFT_MASK + self.handle_list_scroll( + _next=event.direction == Gdk.ScrollDirection.DOWN, + set_entry=widget != self.treeview, + swap=swap, + scroll_window=scroll_window, + ) + return True + + def on_buttonbox_key_press_event(self, widget, event): + """ + Handles when Escape or ALT+arrow up is pressed when focus + is on any of the buttons in the popup + """ + keyval = event.keyval + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + if keyval == Gdk.KEY_Escape or ( + key_is_up(keyval) and state == Gdk.ModifierType.MOD1_MASK + ): + self.popdown() + return True + return False + + # -------------------------------------------------- + # Funcs and callbacks on the buttons to manipulate the list + # -------------------------------------------------- + def add_current_value_to_saved_list(self): + text = self.path_entry.get_text() + text = path_without_trailing_path_sep(text) + values = self.get_values() + if text in values: + # Make the matching value selected + self.set_selected_value(text) + self.handle_list_scroll() + return True + self.add_values([text], scroll_to_row=True, append=False, emit_signal=True) + + def edit_selected_path(self): + path = self.get_selection_path() + if path: + self.on_edit_path(path, self.tree_column) + + def on_button_add_clicked(self, widget): + self.add_current_value_to_saved_list() + self.popup() + + def on_button_edit_clicked(self, widget): + self.edit_selected_path() + + def on_button_remove_clicked(self, widget): + self.remove_selected_path() + return True + + def on_button_up_clicked(self, widget): + self.handle_list_scroll(_next=False, swap=True) + + def on_button_down_clicked(self, widget): + self.handle_list_scroll(_next=True, swap=True) + + def on_button_default_clicked(self, widget): + if self.default_text: + self.set_text(self.default_text, trigger_event=True) + + +class PathCompletionPopup(CompletionList, PathChooserPopup): + """ + + The auto completion popup + + """ + + def __init__(self, builder, path_entry, max_visible_rows): + self.builder = builder + self.treeview = self.builder.get_object('completion_treeview') + self.popup_window = self.builder.get_object('completion_popup_window') + self.path_entry = path_entry + self.text_entry = path_entry.text_entry + self.show_hidden_files = False + + self.signal_handlers = {} + PathChooserPopup.__init__(self, 1, max_visible_rows, self.text_entry) + CompletionList.__init__(self) + + # Add signal handlers + self.signal_handlers[ + 'on_completion_treeview_scroll_event' + ] = self.on_scroll_event + self.signal_handlers[ + 'on_completion_popup_window_focus_out_event' + ] = self.on_completion_popup_window_focus_out_event + + # For when clicking outside the popup + self.signal_handlers[ + 'on_completion_popup_window_button_press_event' + ] = self.on_popup_window_button_press_event + + def popup(self): + """ + Makes the popup visible. + + """ + PathChooserPopup.popup(self) + self.popup_window.grab_focus() + + if not self.treeview.has_focus(): + self.treeview.grab_focus() + + if not self.popup_grab_window(): + self.popup_window.hide() + return + + self.popup_window.grab_add() + self.text_entry.grab_focus() + self.text_entry.set_position(len(self.path_entry.text_entry.get_text())) + self.set_selected_value( + path_without_trailing_path_sep(self.path_entry.get_text()), + select_first=True, + ) + + ################# + # Callbacks + ################# + + def on_completion_popup_window_focus_out_event(self, entry, event): + """ + Popup sometimes loses the focus to the text entry, e.g. when right click + shows a popup menu on a row. This regains the focus. + """ + self.popup_grab_window() + return True + + def on_scroll_event(self, widget, event): + """ + Handles scroll events from the treeview + + """ + x, y = event.window.get_pointer() + self.handle_list_scroll( + _next=event.direction == Gdk.ScrollDirection.DOWN, + set_entry=widget != self.treeview, + scroll_window=True, + ) + path = self.treeview.get_path_at_pos(int(x), int(y)) + if path: + self.handle_list_scroll(path=path[0], _next=None) + return True + + +class PathAutoCompleter: + def __init__(self, builder, path_entry, max_visible_rows): + self.completion_popup = PathCompletionPopup( + builder, path_entry, max_visible_rows + ) + self.path_entry = path_entry + self.dirs_cache = {} + self.use_popup = False + self.auto_complete_enabled = True + self.signal_handlers = self.completion_popup.signal_handlers + + self.signal_handlers[ + 'on_completion_popup_window_key_press_event' + ] = self.on_completion_popup_window_key_press_event + self.signal_handlers[ + 'on_entry_text_delete_text' + ] = self.on_entry_text_delete_text + self.signal_handlers[ + 'on_entry_text_insert_text' + ] = self.on_entry_text_insert_text + self.accelerator_string = Gtk.accelerator_name(Gdk.KEY_Tab, 0) + + def on_entry_text_insert_text(self, entry, new_text, new_text_length, position): + if self.path_entry.get_realized(): + cur_text = self.path_entry.get_text() + pos = entry.get_position() + new_complete_text = cur_text[:pos] + new_text + cur_text[pos:] + # Remove all values from the list that do not start with new_complete_text + self.completion_popup.reduce_values(new_complete_text) + self.completion_popup.set_selected_value( + new_complete_text, select_first=True + ) + if self.completion_popup.is_popped_up(): + self.completion_popup.set_window_position_and_size() + + def on_entry_text_delete_text(self, entry, start, end): + """ + Do completion when characters are removed + + """ + if self.completion_popup.is_popped_up(): + cur_text = self.path_entry.get_text() + new_complete_text = cur_text[:start] + cur_text[end:] + self.do_completion(value=new_complete_text, forward_completion=False) + + def set_use_popup(self, use): + self.use_popup = use + + def on_completion_popup_window_key_press_event(self, entry, event): + """ + Handles key pressed events on the auto-completion popup window + """ + # If on_completion_treeview_key_press_event handles the event, do nothing + ret = self.completion_popup.on_completion_treeview_key_press_event(entry, event) + if ret: + return ret + keyval = event.keyval + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + if ( + self.is_auto_completion_accelerator(keyval, state) + and self.auto_complete_enabled + ): + values_count = self.completion_popup.get_values_count() + if values_count == 1: + self.do_completion() + else: + self.completion_popup.handle_list_scroll(_next=True) + return True + # Buggy stuff (in pygobject?) causing type mismatch between EventKey and GdkEvent. Convert manually... + n = Gdk.Event() + n.type = event.type + n.window = event.window + n.send_event = event.send_event + n.time = event.time + n.state = event.state + n.keyval = event.keyval + n.length = event.length + n.string = event.string + n.hardware_keycode = event.hardware_keycode + n.group = event.group + n.is_modifier = event.is_modifier + self.path_entry.text_entry.emit('key-press-event', n) + + def is_auto_completion_accelerator(self, keyval, state): + return Gtk.accelerator_name(keyval, state) == self.accelerator_string + + def do_completion(self, value=None, forward_completion=True): + if not value: + value = self.path_entry.get_text() + self.path_entry.text_entry.set_position(len(value)) + opts = {} + opts['show_hidden_files'] = self.completion_popup.show_hidden_files + opts['completion_text'] = value + opts['forward_completion'] = forward_completion + self._start_completion(opts) + + def _start_completion(self, args): + args = get_completion_paths(args) + self._end_completion(args) + + def _end_completion(self, args): + value = args['completion_text'] + paths = args['paths'] + + if args['forward_completion']: + common_prefix = os.path.commonprefix(paths) + if len(common_prefix) > len(value): + self.path_entry.set_text( + common_prefix, set_file_chooser_folder=True, trigger_event=True + ) + + self.path_entry.text_entry.set_position(len(self.path_entry.get_text())) + self.completion_popup.set_values(paths, preserve_selection=True) + + if self.use_popup and len(paths) > 1: + self.completion_popup.popup() + elif self.completion_popup.is_popped_up() and args['forward_completion']: + self.completion_popup.popdown() + + +class PathChooserComboBox(Gtk.Box, StoredValuesPopup, GObject.GObject): + __gsignals__ = { + signal: (SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)) + for signal in [ + 'text-changed', + 'accelerator-set', + 'max-rows-changed', + 'list-value-added', + 'list-value-removed', + 'list-values-changed', + 'list-values-reordered', + 'show-path-entry-toggled', + 'show-filechooser-toggled', + 'show-hidden-files-toggled', + 'show-folder-name-on-button', + 'auto-complete-enabled-toggled', + ] + } + + def __init__( + self, + max_visible_rows=20, + auto_complete=True, + use_completer_popup=True, + parent=None, + ): + Gtk.Box.__init__(self) + GObject.GObject.__init__(self) + self.list_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=0) + self._stored_values_popping_down = False + self.filechooser_visible = True + self.filechooser_enabled = True + self.path_entry_visible = True + self.properties_enabled = True + self.show_folder_name_on_button = False + self.setting_accelerator_key = False + self.builder = Gtk.Builder() + self.parent = parent + self.popup_buttonbox = self.builder.get_object('buttonbox') + self.builder.add_from_file( + resource_filename( + __package__, os.path.join('glade', 'path_combo_chooser.ui') + ) + ) + self.button_toggle = self.builder.get_object('button_toggle_dropdown') + self.text_entry = self.builder.get_object('entry_text') + self.open_filechooser_dialog_button = self.builder.get_object( + 'button_open_dialog' + ) + self.filechooser_button = self.open_filechooser_dialog_button + self.filechooserdialog = self.builder.get_object('filechooserdialog') + self.filechooserdialog.set_transient_for(self.parent) + self.filechooser_widget = self.builder.get_object('filechooser_widget') + self.folder_name_label = self.builder.get_object('folder_name_label') + self.default_text = None + self.button_properties = self.builder.get_object('button_properties') + + self.combobox_window = self.builder.get_object('combobox_window') + self.combo_hbox = self.builder.get_object('entry_combobox_hbox') + # Change the parent of the hbox from the glade Window to this hbox. + self.combobox_window.remove(self.combo_hbox) + self.combobox_window = self.get_window() + self.add(self.combo_hbox) + StoredValuesPopup.__init__( + self, self.builder, self, max_visible_rows, self.combo_hbox + ) + + self.tooltips = Gtk.Tooltip() + self.auto_completer = PathAutoCompleter(self.builder, self, max_visible_rows) + self.auto_completer.set_use_popup(use_completer_popup) + self.auto_completer.auto_complete_enabled = auto_complete + self._setup_config_dialog() + + signal_handlers = { + 'on_button_toggle_dropdown_toggled': self._on_button_toggle_dropdown_toggled, + 'on_entry_text_key_press_event': self._on_entry_text_key_press_event, + 'on_stored_values_popup_window_hide': self._on_stored_values_popup_window_hide, + 'on_button_toggle_dropdown_button_press_event': self._on_button_toggle_dropdown_button_press_event, + 'on_entry_combobox_hbox_realize': self._on_entry_combobox_hbox_realize, + 'on_button_open_dialog_clicked': self._on_button_open_dialog_clicked, + 'on_entry_text_focus_out_event': self._on_entry_text_focus_out_event, + 'on_entry_text_changed': self.on_entry_text_changed, + } + signal_handlers.update(self.signal_handlers) + signal_handlers.update(self.auto_completer.signal_handlers) + signal_handlers.update(self.config_dialog_signal_handlers) + self.builder.connect_signals(signal_handlers) + + def get_text(self): + """ + Get the current text in the Entry + """ + return self.text_entry.get_text() + + def set_text( + self, + text, + set_file_chooser_folder=True, + cursor_end=True, + default_text=False, + trigger_event=False, + ): + """ + Set the text for the entry. + + """ + old_text = self.text_entry.get_text() + # We must block the "delete-text" signal to avoid the signal handler being called + self.text_entry.handler_block_by_func( + self.auto_completer.on_entry_text_delete_text + ) + self.text_entry.set_text(text) + self.text_entry.handler_unblock_by_func( + self.auto_completer.on_entry_text_delete_text + ) + + self.text_entry.select_region(0, 0) + self.text_entry.set_position(len(text) if cursor_end else 0) + self.set_selected_value(text, select_first=True) + self.combo_hbox.set_tooltip_text(text) + if default_text: + self.default_text = text + self.button_default.set_tooltip_text( + 'Restore the default value in the text entry:\n%s' % self.default_text + ) + self.button_default.set_sensitive(True) + # Set text for the filechooser dialog button + folder_name = '' + if self.show_folder_name_on_button or not self.path_entry_visible: + folder_name = path_without_trailing_path_sep(text) + if folder_name != '/' and os.path.basename(folder_name): + folder_name = os.path.basename(folder_name) + self.folder_name_label.set_text(folder_name) + # Only trigger event if text has changed + if old_text != text and trigger_event: + self.on_entry_text_changed(self.text_entry) + + def set_sensitive(self, sensitive): + """ + Set the path chooser widgets sensitive + + :param sensitive: if the widget should be sensitive + :type sensitive: bool + + """ + self.text_entry.set_sensitive(sensitive) + self.filechooser_button.set_sensitive(sensitive) + self.button_toggle.set_sensitive(sensitive) + + def get_accelerator_string(self): + return self.auto_completer.accelerator_string + + def set_accelerator_string(self, accelerator): + """ + Set the accelerator string to trigger auto-completion + """ + if accelerator is None: + return + try: + # Verify that the accelerator can be parsed + keyval, mask = Gtk.accelerator_parse(self.auto_completer.accelerator_string) + self.auto_completer.accelerator_string = accelerator + except TypeError as ex: + raise TypeError('TypeError when setting accelerator string: %s' % ex) + + def get_auto_complete_enabled(self): + return self.auto_completer.auto_complete_enabled + + def set_auto_complete_enabled(self, enable): + if not isinstance(enable, bool): + return + self.auto_completer.auto_complete_enabled = enable + + def get_show_folder_name_on_button(self): + return self.show_folder_name_on_button + + def set_show_folder_name_on_button(self, show): + if not isinstance(show, bool): + return + self.show_folder_name_on_button = show + self._set_path_entry_filechooser_widths() + + def get_filechooser_button_enabled(self): + return self.filechooser_enabled + + def set_filechooser_button_enabled(self, enable): + """ + Enable/disable the filechooser button. + + By setting filechooser disabled, in will not be possible + to change the settings in the properties. + """ + if not isinstance(enable, bool): + return + self.filechooser_enabled = enable + if not enable: + self.set_filechooser_button_visible(False, update=False) + + def get_filechooser_button_visible(self): + return self.filechooser_visible + + def set_filechooser_button_visible(self, visible, update=True): + """ + Set file chooser button entry visible + """ + if not isinstance(visible, bool): + return + if update: + self.filechooser_visible = visible + if visible and not self.filechooser_enabled: + return + if visible: + self.filechooser_button.show() + else: + self.filechooser_button.hide() + # Update width properties + self._set_path_entry_filechooser_widths() + + def get_path_entry_visible(self): + return self.path_entry_visible + + def set_path_entry_visible(self, visible): + """ + Set the path entry visible + """ + if not isinstance(visible, bool): + return + self.path_entry_visible = visible + if visible: + self.text_entry.show() + else: + self.text_entry.hide() + self._set_path_entry_filechooser_widths() + + def get_show_hidden_files(self): + return self.auto_completer.completion_popup.show_hidden_files + + def set_show_hidden_files(self, show, do_completion=False, emit_event=False): + """ + Enable/disable showing hidden files on path completion + """ + if not isinstance(show, bool): + return + self.auto_completer.completion_popup.show_hidden_files = show + if do_completion: + self.auto_completer.do_completion() + if emit_event: + self.emit('show-hidden-files-toggled', show) + + def set_enable_properties(self, enable): + """ + Enable/disable the config properties + """ + if not isinstance(enable, bool): + return + self.properties_enabled = enable + if self.properties_enabled: + self.popup_buttonbox.add(self.button_properties) + else: + self.popup_buttonbox.remove(self.button_properties) + + def set_auto_completer_func(self, func): + """ + Set the function to be called when the auto completion + accelerator is triggered. + """ + self.auto_completer._start_completion = func + + def complete(self, args): + """ + Perform the auto completion with the provided paths + """ + self.auto_completer._end_completion(args) + + ############## + # Callbacks and internal functions + ############## + + def on_entry_text_changed(self, entry): + self.emit('text-changed', self.get_text()) + + def _on_entry_text_focus_out_event(self, widget, event): + # FIXME: This causes text to be deselected on right-click. + # Update text on the button label + self.set_text(self.get_text()) + + def _set_path_entry_filechooser_widths(self): + if self.path_entry_visible: + self.combo_hbox.set_child_packing( + self.filechooser_button, 0, 0, 0, Gtk.PackType.START + ) + width, height = self.folder_name_label.get_size_request() + width = 120 + if not self.show_folder_name_on_button: + width = 0 + self.folder_name_label.set_size_request(width, height) + self.combo_hbox.set_child_packing( + self.filechooser_button, 0, 0, 0, Gtk.PackType.START + ) + else: + self.combo_hbox.set_child_packing( + self.filechooser_button, 1, 1, 0, Gtk.PackType.START + ) + self.folder_name_label.set_size_request(-1, -1) + # Update text on the button label + self.set_text(self.get_text()) + + def _on_entry_combobox_hbox_realize(self, widget): + """Must do this when the widget is realized""" + self.set_filechooser_button_visible(self.filechooser_visible) + self.set_path_entry_visible(self.path_entry_visible) + + def _on_button_open_dialog_clicked(self, widget): + self.filechooser_widget.set_current_folder(self.get_text()) + response_id = self.filechooserdialog.run() + + if response_id == 0: + text = self.filechooser_widget.get_filename() + self.set_text(text, trigger_event=True) + self.filechooserdialog.hide() + + def _on_entry_text_key_press_event(self, widget, event): + """ + Listen to key events on the entry widget. + + Arrow up/down will change the value of the entry according to the + current selection in the list. + Enter will show the popup. + + Return True whenever we want no other event listeners to be called. + + """ + # on_entry_text_key_press_event Errors follow here when pressing ALT key while popup is visible") + keyval = event.keyval + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + + # Select new row with arrow up/down is pressed + if key_is_up_or_down(keyval): + self.handle_list_scroll(_next=key_is_down(keyval), set_entry=True) + return True + elif self.auto_completer.is_auto_completion_accelerator(keyval, state): + if self.auto_completer.auto_complete_enabled: + self.auto_completer.do_completion() + return True + # Show popup when Enter is pressed + elif key_is_enter(keyval): + # This sets the toggle active which results in + # on_button_toggle_dropdown_toggled being called which initiates the popup + self.button_toggle.set_active(True) + return True + elif ctrl: + # Swap the show hidden files value on CTRL-h + if is_ascii_value(keyval, 'h'): + # Set show/hide hidden files + self.set_show_hidden_files( + not self.get_show_hidden_files(), emit_event=True + ) + return True + elif is_ascii_value(keyval, 's'): + super().add_current_value_to_saved_list() + return True + elif is_ascii_value(keyval, 'd'): + # Set the default value in the text entry + self.set_text(self.default_text, trigger_event=True) + return True + return False + + def _on_button_toggle_dropdown_toggled(self, button): + """ + Shows the popup when clicking the toggle button. + """ + if self._stored_values_popping_down: + return + self.popup() + + def _on_stored_values_popup_window_hide(self, popup): + """Make sure the button toggle is removed when popup is closed""" + self._stored_values_popping_down = True + self.button_toggle.set_active(False) + self._stored_values_popping_down = False + + ############## + # Config dialog + ############## + + def _on_button_toggle_dropdown_button_press_event(self, widget, event): + """Show config when right clicking dropdown toggle button""" + if not self.properties_enabled: + return False + # This is right click + if event.button == 3: + self._on_button_properties_clicked(widget) + return True + + def _on_button_properties_clicked(self, widget): + self.popdown() + self.enable_completion.set_active(self.get_auto_complete_enabled()) + # Set the value of the label to the current accelerator + keyval, mask = Gtk.accelerator_parse(self.auto_completer.accelerator_string) + self.accelerator_label.set_text(Gtk.accelerator_get_label(keyval, mask)) + self.visible_rows.set_value(self.get_max_popup_rows()) + self.show_filechooser_checkbutton.set_active( + self.get_filechooser_button_visible() + ) + self.show_path_entry_checkbutton.set_active(self.path_entry_visible) + self.show_hidden_files_checkbutton.set_active(self.get_show_hidden_files()) + self.show_folder_name_on_button_checkbutton.set_active( + self.get_show_folder_name_on_button() + ) + self._set_properties_widgets_sensitive(True) + self.config_dialog.show_all() + + def _set_properties_widgets_sensitive(self, val): + self.enable_completion.set_sensitive(val) + self.config_short_cuts_frame.set_sensitive(val) + self.config_general_frame.set_sensitive(val) + self.show_hidden_files_checkbutton.set_sensitive(val) + + def _setup_config_dialog(self): + self.config_dialog = self.builder.get_object('completion_config_dialog') + self.enable_completion = self.builder.get_object( + 'enable_auto_completion_checkbutton' + ) + self.show_filechooser_checkbutton = self.builder.get_object( + 'show_filechooser_checkbutton' + ) + self.show_path_entry_checkbutton = self.builder.get_object( + 'show_path_entry_checkbutton' + ) + set_key_button = self.builder.get_object('set_completion_accelerator_button') + default_set_accelerator_tooltip = set_key_button.get_tooltip_text() + self.config_short_cuts_frame = self.builder.get_object( + 'config_short_cuts_frame' + ) + self.config_general_frame = self.builder.get_object('config_general_frame') + self.accelerator_label = self.builder.get_object('completion_accelerator_label') + self.visible_rows = self.builder.get_object('visible_rows_spinbutton') + self.visible_rows_label = self.builder.get_object('visible_rows_label') + self.show_hidden_files_checkbutton = self.builder.get_object( + 'show_hidden_files_checkbutton' + ) + self.show_folder_name_on_button_checkbutton = self.builder.get_object( + 'show_folder_name_on_button_checkbutton' + ) + self.config_dialog.set_transient_for(self.parent) + + def on_close(widget, event=None): + if not self.setting_accelerator_key: + self.config_dialog.hide() + else: + stop_setting_accelerator() + return True + + def on_enable_completion_toggled(widget): + self.set_auto_complete_enabled(self.enable_completion.get_active()) + self.emit( + 'auto-complete-enabled-toggled', self.enable_completion.get_active() + ) + + def on_show_filechooser_toggled(widget): + self.set_filechooser_button_visible( + self.show_filechooser_checkbutton.get_active() + ) + self.emit( + 'show-filechooser-toggled', + self.show_filechooser_checkbutton.get_active(), + ) + self.show_folder_name_on_button_checkbutton.set_sensitive( + self.show_path_entry_checkbutton.get_active() + and self.show_filechooser_checkbutton.get_active() + ) + if not self.filechooser_visible and not self.path_entry_visible: + self.show_path_entry_checkbutton.set_active(True) + on_show_path_entry_toggled(None) + + def on_show_path_entry_toggled(widget): + self.set_path_entry_visible(self.show_path_entry_checkbutton.get_active()) + self.emit( + 'show-path-entry-toggled', self.show_path_entry_checkbutton.get_active() + ) + self.show_folder_name_on_button_checkbutton.set_sensitive( + self.show_path_entry_checkbutton.get_active() + and self.show_filechooser_checkbutton.get_active() + ) + if not self.filechooser_visible and not self.path_entry_visible: + self.show_filechooser_checkbutton.set_active(True) + on_show_filechooser_toggled(None) + + def on_show_folder_name_on_button(widget): + self.set_show_folder_name_on_button( + self.show_folder_name_on_button_checkbutton.get_active() + ) + self._set_path_entry_filechooser_widths() + self.emit( + 'show-folder-name-on-button', + self.show_folder_name_on_button_checkbutton.get_active(), + ) + + def on_show_hidden_files_toggled(widget): + self.set_show_hidden_files( + self.show_hidden_files_checkbutton.get_active(), emit_event=True + ) + + def on_max_rows_changed(widget): + self.set_max_popup_rows(self.visible_rows.get_value_as_int()) + self.emit('max-rows-changed', self.visible_rows.get_value_as_int()) + + def set_accelerator(widget): + self.setting_accelerator_key = True + set_key_button.set_tooltip_text( + 'Press the accelerator keys for triggering auto-completion' + ) + self._set_properties_widgets_sensitive(False) + return True + + def stop_setting_accelerator(): + self.setting_accelerator_key = False + self._set_properties_widgets_sensitive(True) + set_key_button.set_active(False) + # Restore default tooltip + set_key_button.set_tooltip_text(default_set_accelerator_tooltip) + + def on_completion_config_dialog_key_release_event(widget, event): + # We are listening for a new key + if set_key_button.get_active(): + state = event.get_state() & Gtk.accelerator_get_default_mod_mask() + accelerator_mask = state.numerator + # If e.g. only CTRL key is pressed. + if not Gtk.accelerator_valid(event.keyval, accelerator_mask): + accelerator_mask = 0 + self.auto_completer.accelerator_string = Gtk.accelerator_name( + event.keyval, accelerator_mask + ) + self.accelerator_label.set_text( + Gtk.accelerator_get_label(event.keyval, accelerator_mask) + ) + self.emit('accelerator-set', self.auto_completer.accelerator_string) + stop_setting_accelerator() + return True + else: + keyval = event.keyval + ctrl = event.get_state() & Gdk.ModifierType.CONTROL_MASK + if ctrl: + # Set show/hide hidden files + if is_ascii_value(keyval, 'h'): + self.show_hidden_files_checkbutton.set_active( + not self.get_show_hidden_files() + ) + return True + + def on_set_completion_accelerator_button_clicked(widget): + if not set_key_button.get_active(): + stop_setting_accelerator() + return True + + self.config_dialog_signal_handlers = { + 'on_enable_auto_completion_checkbutton_toggled': on_enable_completion_toggled, + 'on_show_filechooser_checkbutton_toggled': on_show_filechooser_toggled, + 'on_show_path_entry_checkbutton_toggled': on_show_path_entry_toggled, + 'on_show_folder_name_on_button_checkbutton_toggled': on_show_folder_name_on_button, + 'on_config_dialog_button_close_clicked': on_close, + 'on_visible_rows_spinbutton_value_changed': on_max_rows_changed, + 'on_completion_config_dialog_delete_event': on_close, + 'on_set_completion_accelerator_button_pressed': set_accelerator, + 'on_completion_config_dialog_key_release_event': on_completion_config_dialog_key_release_event, + 'on_set_completion_accelerator_button_clicked': on_set_completion_accelerator_button_clicked, + 'on_show_hidden_files_checkbutton_toggled': on_show_hidden_files_toggled, + } + + +GObject.type_register(PathChooserComboBox) + + +if __name__ == '__main__': + import signal + + # necessary to exit with CTRL-C (https://bugzilla.gnome.org/show_bug.cgi?id=622084) + signal.signal(signal.SIGINT, signal.SIG_DFL) + import sys + + w = Gtk.Window() + w.set_position(Gtk.WindowPosition.CENTER) + w.set_size_request(600, -1) + w.set_title('ComboEntry example') + w.connect('delete-event', Gtk.main_quit) + + box1 = Gtk.Box.new(Gtk.Orientation.VERTICAL, spacing=0) + + def get_resource2(filename): + return f'{os.path.abspath(os.path.dirname(sys.argv[0]))}/glade/{filename}' + + # Override get_resource which fetches from deluge install + # get_resource = get_resource2 + + entry1 = PathChooserComboBox(max_visible_rows=15) + entry2 = PathChooserComboBox() + + box1.add(entry1) + box1.add(entry2) + + test_paths = [ + '/home/bro/Downloads', + '/media/Movies-HD', + '/media/torrent/in', + '/media/Live-show/Misc', + '/media/Live-show/Consert', + '/media/Series/1/', + '/media/Series/2', + '/media/Series/17', + '/media/Series/18', + '/media/Series/19', + ] + + entry1.add_values(test_paths) + entry1.set_text('/home/bro/', default_text=True) + entry2.set_text( + '/home/bro/programmer/deluge/deluge-yarss-plugin/build/lib/yarss2/include/bs4/tests/', + cursor_end=False, + ) + + entry2.set_filechooser_button_visible(False) + # entry2.set_enable_properties(False) + entry2.set_filechooser_button_enabled(False) + + def list_value_added_event(widget, values): + print('Current list values:', widget.get_values()) + + entry1.connect('list-value-added', list_value_added_event) + entry2.connect('list-value-added', list_value_added_event) + w.add(box1) + w.show_all() + Gtk.main() diff --git a/deluge/ui/gtk3/peers_tab.py b/deluge/ui/gtk3/peers_tab.py new file mode 100644 index 0000000..5768fbe --- /dev/null +++ b/deluge/ui/gtk3/peers_tab.py @@ -0,0 +1,382 @@ +# +# Copyright (C) 2008 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. +# + +import logging +import os.path + +from gi.repository.GdkPixbuf import Pixbuf +from gi.repository.Gtk import ( + Builder, + CellRendererPixbuf, + CellRendererProgress, + CellRendererText, + ListStore, + TreeViewColumn, + TreeViewColumnSizing, +) + +import deluge.common +import deluge.component as component +from deluge.ui.client import client +from deluge.ui.countries import COUNTRIES + +from .common import ( + icon_downloading, + icon_seeding, + load_pickled_state_file, + parse_ip_port, + save_pickled_state_file, +) +from .torrentdetails import Tab +from .torrentview_data_funcs import ( + cell_data_peer_progress, + cell_data_speed_down, + cell_data_speed_up, +) + +log = logging.getLogger(__name__) + + +class PeersTab(Tab): + def __init__(self): + super().__init__('Peers', 'peers_tab', 'peers_tab_label') + + self.peer_menu = self.main_builder.get_object('menu_peer_tab') + component.get('MainWindow').connect_signals(self) + + self.listview = self.main_builder.get_object('peers_listview') + self.listview.props.has_tooltip = True + self.listview.connect('button-press-event', self._on_button_press_event) + self.listview.connect('query-tooltip', self._on_query_tooltip) + + # flag, ip, client, downspd, upspd, country code, int_ip, seed/peer icon, progress + self.liststore = ListStore( + Pixbuf, str, str, int, int, str, float, Pixbuf, float + ) + self.cached_flag_pixbufs = {} + + self.seed_pixbuf = icon_seeding + self.peer_pixbuf = icon_downloading + + # key is ip address, item is row iter + self.peers = {} + + # Country column + column = TreeViewColumn() + render = CellRendererPixbuf() + column.pack_start(render, False) + column.add_attribute(render, 'pixbuf', 0) + column.set_sort_column_id(5) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(20) + column.set_reorderable(True) + self.listview.append_column(column) + + # Address column + column = TreeViewColumn(_('Address')) + render = CellRendererPixbuf() + column.pack_start(render, False) + column.add_attribute(render, 'pixbuf', 7) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 1) + column.set_sort_column_id(6) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + # Client column + column = TreeViewColumn(_('Client')) + render = CellRendererText() + column.pack_start(render, False) + column.add_attribute(render, 'text', 2) + column.set_sort_column_id(2) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + # Progress column + column = TreeViewColumn(_('Progress')) + render = CellRendererProgress() + column.pack_start(render, True) + column.set_cell_data_func(render, cell_data_peer_progress, 8) + column.set_sort_column_id(8) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(100) + column.set_reorderable(True) + self.listview.append_column(column) + + # Down Speed column + column = TreeViewColumn(_('Down Speed')) + render = CellRendererText() + column.pack_start(render, False) + column.set_cell_data_func(render, cell_data_speed_down, 3) + column.set_sort_column_id(3) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(50) + column.set_reorderable(True) + self.listview.append_column(column) + + # Up Speed column + column = TreeViewColumn(_('Up Speed')) + render = CellRendererText() + column.pack_start(render, False) + column.set_cell_data_func(render, cell_data_speed_up, 4) + column.set_sort_column_id(4) + column.set_clickable(True) + column.set_resizable(True) + column.set_expand(False) + column.set_min_width(50) + # Bugfix: Last column needs max_width set to stop scrollbar appearing + column.set_max_width(150) + column.set_reorderable(True) + self.listview.append_column(column) + + self.listview.set_model(self.liststore) + + self.load_state() + + self.torrent_id = None + + def save_state(self): + # Get the current sort order of the view + column_id, sort_order = self.liststore.get_sort_column_id() + + # Setup state dict + state = { + 'columns': {}, + 'sort_id': column_id, + 'sort_order': int(sort_order) if sort_order else None, + } + + for index, column in enumerate(self.listview.get_columns()): + state['columns'][column.get_title()] = { + 'position': index, + 'width': column.get_width(), + } + save_pickled_state_file('peers_tab.state', state) + + def load_state(self): + state = load_pickled_state_file('peers_tab.state') + + if state is None: + return + + if len(state['columns']) != len(self.listview.get_columns()): + log.warning('peers_tab.state is not compatible! rejecting..') + return + + if state['sort_id'] and state['sort_order'] is not None: + self.liststore.set_sort_column_id(state['sort_id'], state['sort_order']) + + for index, column in enumerate(self.listview.get_columns()): + cname = column.get_title() + if cname in state['columns']: + cstate = state['columns'][cname] + column.set_sizing(TreeViewColumnSizing.FIXED) + column.set_fixed_width(cstate['width'] if cstate['width'] > 0 else 10) + if state['sort_id'] == index and state['sort_order'] is not None: + column.set_sort_indicator(True) + column.set_sort_order(state['sort_order']) + if cstate['position'] != index: + # Column is in wrong position + if cstate['position'] == 0: + self.listview.move_column_after(column, None) + elif ( + self.listview.get_columns()[cstate['position'] - 1].get_title() + != cname + ): + self.listview.move_column_after( + column, self.listview.get_columns()[cstate['position'] - 1] + ) + + def update(self): + # Get the first selected torrent + torrent_id = component.get('TorrentView').get_selected_torrents() + + # Only use the first torrent in the list or return if None selected + if len(torrent_id) != 0: + torrent_id = torrent_id[0] + else: + # No torrent is selected in the torrentview + self.liststore.clear() + return + + if torrent_id != self.torrent_id: + # We only want to do this if the torrent_id has changed + self.liststore.clear() + self.peers = {} + self.torrent_id = torrent_id + + component.get('SessionProxy').get_torrent_status( + torrent_id, ['peers'] + ).addCallback(self._on_get_torrent_status) + + def get_flag_pixbuf(self, country): + if not country.strip(): + return None + + if country not in self.cached_flag_pixbufs: + # We haven't created a pixbuf for this country yet + try: + self.cached_flag_pixbufs[country] = Pixbuf.new_from_file( + deluge.common.resource_filename( + 'deluge', + os.path.join( + 'ui', 'data', 'pixmaps', 'flags', country.lower() + '.png' + ), + ) + ) + except Exception as ex: + log.debug('Unable to load flag: %s', ex) + return None + + return self.cached_flag_pixbufs[country] + + def _on_get_torrent_status(self, status): + new_ips = set() + for peer in status['peers']: + new_ips.add(peer['ip']) + if peer['ip'] in self.peers: + # We already have this peer in our list, so lets just update it + row = self.peers[peer['ip']] + if not self.liststore.iter_is_valid(row): + # This iter is invalid, delete it and continue to next iteration + del self.peers[peer['ip']] + continue + values = self.liststore.get(row, 3, 4, 5, 7, 8) + if peer['down_speed'] != values[0]: + self.liststore.set_value(row, 3, peer['down_speed']) + if peer['up_speed'] != values[1]: + self.liststore.set_value(row, 4, peer['up_speed']) + if peer['country'] != values[2]: + self.liststore.set_value(row, 5, peer['country']) + self.liststore.set_value( + row, 0, self.get_flag_pixbuf(peer['country']) + ) + if peer['seed']: + icon = self.seed_pixbuf + else: + icon = self.peer_pixbuf + + if icon != values[3]: + self.liststore.set_value(row, 7, icon) + + if peer['progress'] != values[4]: + self.liststore.set_value(row, 8, peer['progress']) + else: + # Peer is not in list so we need to add it + + # Create an int IP address for sorting purposes + if peer['ip'].count(':') == 1: + # This is an IPv4 address + ip_int = sum( + int(byte) << shift + for byte, shift in zip( + peer['ip'].split(':')[0].split('.'), (24, 16, 8, 0) + ) + ) + peer_ip = peer['ip'] + else: + # This is an IPv6 address + import binascii + import socket + + # Split out the :port + ip = ':'.join(peer['ip'].split(':')[:-1]) + ip_int = int( + binascii.hexlify(socket.inet_pton(socket.AF_INET6, ip)), 16 + ) + peer_ip = '[{}]:{}'.format(ip, peer['ip'].split(':')[-1]) + + if peer['seed']: + icon = self.seed_pixbuf + else: + icon = self.peer_pixbuf + + row = self.liststore.append( + [ + self.get_flag_pixbuf(peer['country']), + peer_ip, + peer['client'], + peer['down_speed'], + peer['up_speed'], + peer['country'], + float(ip_int), + icon, + peer['progress'], + ] + ) + + self.peers[peer['ip']] = row + + # Now we need to remove any ips that were not in status["peers"] list + for ip in set(self.peers).difference(new_ips): + self.liststore.remove(self.peers[ip]) + del self.peers[ip] + + def clear(self): + self.liststore.clear() + + def _on_button_press_event(self, widget, event): + """This is a callback for showing the right-click context menu.""" + log.debug('on_button_press_event') + # We only care about right-clicks + if self.torrent_id and event.button == 3: + self.peer_menu.popup(None, None, None, None, event.button, event.time) + return True + + def _on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip): + is_tooltip, x, y, model, path, _iter = widget.get_tooltip_context( + x, y, keyboard_tip + ) + if is_tooltip: + country_code = model.get(_iter, 5)[0] + if country_code != ' ' and country_code in COUNTRIES: + tooltip.set_text(COUNTRIES[country_code]) + # widget here is self.listview + widget.set_tooltip_cell(tooltip, path, widget.get_column(0), None) + return True + return False + + def on_menuitem_add_peer_activate(self, menuitem): + """This is a callback for manually adding a peer""" + log.debug('on_menuitem_add_peer') + builder = Builder() + builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'connect_peer_dialog.ui') + ) + ) + peer_dialog = builder.get_object('connect_peer_dialog') + txt_ip = builder.get_object('txt_ip') + response = peer_dialog.run() + + if response: + value = txt_ip.get_text() + ip, port = parse_ip_port(value) + if ip and port: + log.info('Adding peer IP: %s port: %s to %s', ip, port, self.torrent_id) + client.core.connect_peer(self.torrent_id, ip, port) + else: + log.error('Error parsing peer "%s"', value) + + peer_dialog.destroy() + return True diff --git a/deluge/ui/gtk3/piecesbar.py b/deluge/ui/gtk3/piecesbar.py new file mode 100644 index 0000000..a5bf865 --- /dev/null +++ b/deluge/ui/gtk3/piecesbar.py @@ -0,0 +1,225 @@ +# +# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> +# +# 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. +# + +from math import pi + +import gi # isort:skip (Version check required before import). + +gi.require_version('PangoCairo', '1.0') +gi.require_foreign('cairo') +gi.require_version('cairo', '1.0') + +# isort:imports-thirdparty +import cairo # Backward compat cairo <= 1.15 +from gi.repository import PangoCairo +from gi.repository.Gtk import DrawingArea, ProgressBar, StateFlags +from gi.repository.Pango import SCALE, Weight + +# isort:imports-firstparty +from deluge.configmanager import ConfigManager + +COLOR_STATES = ['missing', 'waiting', 'downloading', 'completed'] + + +class PiecesBar(DrawingArea): + # Draw in response to an draw + __gsignals__ = {'draw': 'override'} + + def __init__(self): + super().__init__() + # Get progress bar styles, in order to keep font consistency + pb = ProgressBar() + pb_style = pb.get_style_context() + # Get a copy of Pango.FontDescription since original needs freed. + self.text_font = pb_style.get_property('font', StateFlags.NORMAL).copy() + self.text_font.set_weight(Weight.BOLD) + # Done with the ProgressBar styles, don't keep refs of it + del pb, pb_style + + self.set_size_request(-1, 25) + self.gtkui_config = ConfigManager('gtk3ui.conf') + + self.width = self.prev_width = 0 + self.height = self.prev_height = 0 + self.pieces = self.prev_pieces = () + self.num_pieces = None + self.text = self.prev_text = '' + self.fraction = self.prev_fraction = 0 + self.progress_overlay = self.text_overlay = self.pieces_overlay = None + + self.connect('size-allocate', self.do_size_allocate_event) + self.show() + + def do_size_allocate_event(self, widget, size): + self.prev_width = self.width + self.width = size.width + self.prev_height = self.height + self.height = size.height + + # Handle the draw by drawing + def do_draw(self, ctx): + ctx.set_line_width(max(ctx.device_to_user_distance(0.5, 0.5))) + + # Restrict Cairo to the exposed area; avoid extra work + self.roundcorners_clipping(ctx) + + self.draw_pieces(ctx) + self.draw_progress_overlay(ctx) + self.write_text(ctx) + self.roundcorners_border(ctx) + + # Drawn once, update width, height + if self.resized(): + self.prev_width = self.width + self.prev_height = self.height + + def roundcorners_clipping(self, ctx): + self.create_roundcorners_subpath(ctx, 0, 0, self.width, self.height) + ctx.clip() + + def roundcorners_border(self, ctx): + self.create_roundcorners_subpath(ctx, 0.5, 0.5, self.width - 1, self.height - 1) + ctx.set_source_rgba(0, 0, 0, 0.9) + ctx.stroke() + + @staticmethod + def create_roundcorners_subpath(ctx, x, y, width, height): + aspect = 1.0 + corner_radius = height / 10 + radius = corner_radius / aspect + degrees = pi / 180 + ctx.new_sub_path() + ctx.arc(x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees) + ctx.arc( + x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees + ) + ctx.arc(x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees) + ctx.arc(x + radius, y + radius, radius, 180 * degrees, 270 * degrees) + ctx.close_path() + + def draw_pieces(self, ctx): + if not self.num_pieces: + return + + if ( + self.resized() + or self.pieces != self.prev_pieces + or self.pieces_overlay is None + ): + # Need to recreate the cache drawing + self.pieces_overlay = cairo.ImageSurface( + cairo.FORMAT_ARGB32, self.width, self.height + ) + pieces_ctx = cairo.Context(self.pieces_overlay) + + if self.pieces: + pieces = self.pieces + elif self.num_pieces: + # Completed torrents do not send any pieces so create list using 'completed' state. + pieces = [COLOR_STATES.index('completed')] * self.num_pieces + start_pos = 0 + piece_width = self.width / len(pieces) + pieces_colors = [ + [ + color / 65535 + for color in self.gtkui_config['pieces_color_%s' % state] + ] + for state in COLOR_STATES + ] + for state in pieces: + pieces_ctx.set_source_rgb(*pieces_colors[state]) + pieces_ctx.rectangle(start_pos, 0, piece_width, self.height) + pieces_ctx.fill() + start_pos += piece_width + + ctx.set_source_surface(self.pieces_overlay) + ctx.paint() + + def draw_progress_overlay(self, ctx): + if not self.text: + return + + if ( + self.resized() + or self.fraction != self.prev_fraction + or self.progress_overlay is None + ): + # Need to recreate the cache drawing + self.progress_overlay = cairo.ImageSurface( + cairo.FORMAT_ARGB32, self.width, self.height + ) + progress_ctx = cairo.Context(self.progress_overlay) + progress_ctx.set_source_rgba(0.1, 0.1, 0.1, 0.3) # Transparent + progress_ctx.rectangle(0, 0, self.width * self.fraction, self.height) + progress_ctx.fill() + ctx.set_source_surface(self.progress_overlay) + ctx.paint() + + def write_text(self, ctx): + if not self.text: + return + + if self.resized() or self.text != self.prev_text or self.text_overlay is None: + # Need to recreate the cache drawing + self.text_overlay = cairo.ImageSurface( + cairo.FORMAT_ARGB32, self.width, self.height + ) + text_ctx = cairo.Context(self.text_overlay) + pl = PangoCairo.create_layout(text_ctx) + pl.set_font_description(self.text_font) + pl.set_width(-1) # No text wrapping + pl.set_text(self.text, -1) + plsize = pl.get_size() + text_width = plsize[0] // SCALE + text_height = plsize[1] // SCALE + area_width_without_text = self.width - text_width + area_height_without_text = self.height - text_height + text_ctx.move_to( + area_width_without_text // 2, area_height_without_text // 2 + ) + text_ctx.set_source_rgb(1, 1, 1) + PangoCairo.update_layout(text_ctx, pl) + PangoCairo.show_layout(text_ctx, pl) + ctx.set_source_surface(self.text_overlay) + ctx.paint() + + def resized(self): + return self.prev_width != self.width or self.prev_height != self.height + + def set_fraction(self, fraction): + self.prev_fraction = self.fraction + self.fraction = fraction + + def get_fraction(self): + return self.fraction + + def get_text(self): + return self.text + + def set_text(self, text): + self.prev_text = self.text + self.text = text + + def set_pieces(self, pieces, num_pieces): + self.prev_pieces = self.pieces + self.pieces = pieces + self.num_pieces = num_pieces + + def get_pieces(self): + return self.pieces + + def clear(self): + self.pieces = self.prev_pieces = () + self.num_pieces = None + self.text = self.prev_text = '' + self.fraction = self.prev_fraction = 0 + self.progress_overlay = self.text_overlay = self.pieces_overlay = None + self.update() + + def update(self): + self.queue_draw() diff --git a/deluge/ui/gtk3/pluginmanager.py b/deluge/ui/gtk3/pluginmanager.py new file mode 100644 index 0000000..63353c0 --- /dev/null +++ b/deluge/ui/gtk3/pluginmanager.py @@ -0,0 +1,134 @@ +# +# Copyright (C) 2007, 2008 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. +# + +import logging + +import deluge.component as component +import deluge.pluginmanagerbase +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +log = logging.getLogger(__name__) + + +class PluginManager(deluge.pluginmanagerbase.PluginManagerBase, component.Component): + def __init__(self): + component.Component.__init__(self, 'PluginManager') + self.config = ConfigManager('gtk3ui.conf') + deluge.pluginmanagerbase.PluginManagerBase.__init__( + self, 'gtk3ui.conf', 'deluge.plugin.gtk3ui' + ) + + self.hooks = {'on_apply_prefs': [], 'on_show_prefs': []} + + client.register_event_handler( + 'PluginEnabledEvent', self._on_plugin_enabled_event + ) + client.register_event_handler( + 'PluginDisabledEvent', self._on_plugin_disabled_event + ) + + def register_hook(self, hook, function): + """Register a hook function with the plugin manager""" + try: + self.hooks[hook].append(function) + except KeyError: + log.warning('Plugin attempting to register invalid hook.') + + def deregister_hook(self, hook, function): + """Deregisters a hook function""" + try: + self.hooks[hook].remove(function) + except KeyError: + log.warning('Unable to deregister hook %s', hook) + + def start(self): + """Start the plugin manager""" + # Update the enabled_plugins from the core + client.core.get_enabled_plugins().addCallback(self._on_get_enabled_plugins) + for instance in self.plugins.values(): + component.start([instance.plugin._component_name]) + + def stop(self): + # Disable the plugins + self.disable_plugins() + + def update(self): + pass + + def _on_get_enabled_plugins(self, enabled_plugins): + log.debug('Core has these plugins enabled: %s', enabled_plugins) + for plugin in enabled_plugins: + self.enable_plugin(plugin) + + def _on_plugin_enabled_event(self, name): + try: + self.enable_plugin(name) + except Exception as ex: + log.warning('Failed to enable plugin "%s": ex: %s', name, ex) + + self.run_on_show_prefs() + + def _on_plugin_disabled_event(self, name): + self.disable_plugin(name) + + # Hook functions + def run_on_show_prefs(self): + """This hook is run before the user is shown the preferences dialog. + It is designed so that plugins can update their preference page with + the config.""" + log.debug('run_on_show_prefs') + for function in self.hooks['on_show_prefs']: + function() + + def run_on_apply_prefs(self): + """This hook is run after the user clicks Apply or OK in the preferences + dialog. + """ + log.debug('run_on_apply_prefs') + for function in self.hooks['on_apply_prefs']: + function() + + # Plugin functions.. will likely move to own class.. + + def add_torrentview_text_column(self, *args, **kwargs): + return component.get('TorrentView').add_text_column(*args, **kwargs) + + def remove_torrentview_column(self, *args): + return component.get('TorrentView').remove_column(*args) + + def add_toolbar_separator(self): + return component.get('ToolBar').add_separator() + + def add_toolbar_button(self, *args, **kwargs): + return component.get('ToolBar').add_toolbutton(*args, **kwargs) + + def remove_toolbar_button(self, *args): + return component.get('ToolBar').remove(*args) + + def add_torrentmenu_menu(self, *args): + return component.get('MenuBar').torrentmenu.append(*args) + + def add_torrentmenu_separator(self): + return component.get('MenuBar').add_torrentmenu_separator() + + def remove_torrentmenu_item(self, *args): + return component.get('MenuBar').torrentmenu.remove(*args) + + def add_preferences_page(self, *args): + return component.get('Preferences').add_page(*args) + + def remove_preferences_page(self, *args): + return component.get('Preferences').remove_page(*args) + + def update_torrent_view(self, *args): + return component.get('TorrentView').update(*args) + + def get_selected_torrents(self): + """Returns a list of the selected torrent_ids""" + return component.get('TorrentView').get_selected_torrents() diff --git a/deluge/ui/gtk3/preferences.py b/deluge/ui/gtk3/preferences.py new file mode 100644 index 0000000..cd67a5b --- /dev/null +++ b/deluge/ui/gtk3/preferences.py @@ -0,0 +1,1537 @@ +# +# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> +# +# 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. +# + +import logging +import os +from hashlib import sha1 as sha +from urllib.parse import urlparse + +from gi import require_version +from gi.repository import GObject, Gtk +from gi.repository.Gdk import Color + +import deluge.common +import deluge.component as component +from deluge.configmanager import ConfigManager, get_config_dir +from deluge.decorators import maybe_coroutine +from deluge.error import AuthManagerError, NotAuthorizedError +from deluge.i18n import get_languages +from deluge.ui.client import client +from deluge.ui.common import DISK_CACHE_KEYS, PREFS_CATOG_TRANS + +from .common import associate_magnet_links, get_clipboard_text, get_deluge_icon +from .dialogs import AccountDialog, ErrorDialog, InformationDialog, YesNoDialog +from .path_chooser import PathChooser + +try: + try: + require_version('AyatanaAppIndicator3', '0.1') + from gi.repository import AyatanaAppIndicator3 # noqa: F401 + except (ValueError, ImportError): + require_version('AppIndicator3', '0.1') + from gi.repository import AppIndicator3 # noqa: F401 +except (ImportError, ValueError): + appindicator = False +else: + appindicator = True + +log = logging.getLogger(__name__) + +ACCOUNTS_USERNAME, ACCOUNTS_LEVEL, ACCOUNTS_PASSWORD = list(range(3)) +COLOR_MISSING, COLOR_WAITING, COLOR_DOWNLOADING, COLOR_COMPLETED = list(range(4)) + +COLOR_STATES = { + 'missing': COLOR_MISSING, + 'waiting': COLOR_WAITING, + 'downloading': COLOR_DOWNLOADING, + 'completed': COLOR_COMPLETED, +} + + +class Preferences(component.Component): + def __init__(self): + component.Component.__init__(self, 'Preferences') + self.builder = Gtk.Builder() + self.builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'preferences_dialog.ui') + ) + ) + self.pref_dialog = self.builder.get_object('pref_dialog') + self.pref_dialog.set_transient_for(component.get('MainWindow').window) + self.pref_dialog.set_icon(get_deluge_icon()) + self.treeview = self.builder.get_object('treeview') + self.notebook = self.builder.get_object('notebook') + self.gtkui_config = ConfigManager('gtk3ui.conf') + self.window_open = False + + self.load_pref_dialog_state() + + self.builder.get_object('image_magnet').set_from_file( + deluge.common.get_pixmap('magnet16.png') + ) + + # Hide the unused associate magnet button on OSX see: #2420 + if deluge.common.osx_check(): + self.builder.get_object('button_associate_magnet').hide() + + # Setup the liststore for the categories (tab pages) + self.liststore = Gtk.ListStore(int, str, str) + self.treeview.set_model(self.liststore) + render = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(None, render, text=2) + self.treeview.append_column(column) + + # Add the default categories + prefs_categories = ( + 'interface', + 'downloads', + 'bandwidth', + 'queue', + 'network', + 'proxy', + 'cache', + 'other', + 'daemon', + 'plugins', + ) + for idx, category in enumerate(prefs_categories): + self.liststore.append([idx, category, PREFS_CATOG_TRANS[category]]) + + # Add and set separator after Plugins. + def set_separator(model, _iter, data=None): + entry = deluge.common.decode_bytes(model.get_value(_iter, 1)) + if entry == '_separator_': + return True + + self.treeview.set_row_separator_func(set_separator, None) + self.liststore.append([len(self.liststore), '_separator_', '']) + # Add a dummy notebook page to keep indexing synced with liststore. + self.notebook.append_page(Gtk.HSeparator()) + + # Setup accounts tab lisview + self.accounts_levels_mapping = None + self.accounts_liststore = Gtk.ListStore(str, str, str, int) + self.accounts_liststore.set_sort_column_id( + ACCOUNTS_USERNAME, Gtk.SortType.ASCENDING + ) + self.accounts_listview = self.builder.get_object('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.builder.get_object('AccountsFrame') + + # Setup plugin tab listview + # The third entry is for holding translated plugin names + self.plugin_liststore = Gtk.ListStore(str, bool, str) + self.plugin_liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) + self.plugin_listview = self.builder.get_object('plugin_listview') + self.plugin_listview.set_model(self.plugin_liststore) + render = Gtk.CellRendererToggle() + render.connect('toggled', self.on_plugin_toggled) + render.set_property('activatable', True) + self.plugin_listview.append_column( + Gtk.TreeViewColumn(_('Enabled'), render, active=1) + ) + self.plugin_listview.append_column( + Gtk.TreeViewColumn(_('Plugin'), Gtk.CellRendererText(), text=2) + ) + + # Connect to the 'changed' event of TreeViewSelection to get selection + # changes. + self.treeview.get_selection().connect('changed', self.on_selection_changed) + + self.plugin_listview.get_selection().connect( + 'changed', self.on_plugin_selection_changed + ) + + self.builder.connect_signals(self) + + # Radio buttons to choose between systray and appindicator + self.builder.get_object('alignment_tray_type').set_visible(appindicator) + + # Initialize a binding for dark theme + Gtk.Settings.get_default().bind_property( + 'gtk-application-prefer-dark-theme', + self.builder.get_object('chk_prefer_dark_theme'), + 'active', + GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE, + ) + + from .gtkui import DEFAULT_PREFS + + self.COLOR_DEFAULTS = {} + for key in ('missing', 'waiting', 'downloading', 'completed'): + self.COLOR_DEFAULTS[key] = DEFAULT_PREFS['pieces_color_%s' % key][:] + del DEFAULT_PREFS + + # These get updated by requests done to the core + self.all_plugins = [] + self.enabled_plugins = [] + + self.setup_path_choosers() + self.load_languages() + + def setup_path_choosers(self): + self.download_location_hbox = self.builder.get_object( + 'hbox_download_to_path_chooser' + ) + self.download_location_path_chooser = PathChooser( + 'download_location_paths_list', parent=self.pref_dialog + ) + self.download_location_hbox.add(self.download_location_path_chooser) + self.download_location_hbox.show_all() + + self.move_completed_hbox = self.builder.get_object( + 'hbox_move_completed_to_path_chooser' + ) + self.move_completed_path_chooser = PathChooser( + 'move_completed_paths_list', parent=self.pref_dialog + ) + self.move_completed_hbox.add(self.move_completed_path_chooser) + self.move_completed_hbox.show_all() + + self.copy_torrents_to_hbox = self.builder.get_object( + 'hbox_copy_torrent_files_path_chooser' + ) + self.copy_torrent_files_path_chooser = PathChooser( + 'copy_torrent_files_to_paths_list', parent=self.pref_dialog + ) + self.copy_torrents_to_hbox.add(self.copy_torrent_files_path_chooser) + self.copy_torrents_to_hbox.show_all() + + def load_languages(self): + self.language_combo = self.builder.get_object('combobox_language') + self.language_checkbox = self.builder.get_object('checkbutton_language') + lang_model = self.language_combo.get_model() + langs = get_languages() + index = -1 + for i, l in enumerate(langs): + lang_code, name = l + lang_model.append([lang_code, name]) + if self.gtkui_config['language'] == lang_code: + index = i + + if self.gtkui_config['language'] is None: + self.language_checkbox.set_active(True) + self.language_combo.set_visible(False) + else: + self.language_combo.set_visible(True) + if index != -1: + self.language_combo.set_active(index) + + def __del__(self): + del self.gtkui_config + + def add_page(self, name, widget): + """Add a another page to the notebook""" + # Create a header and scrolled window for the preferences tab + parent = widget.get_parent() + if parent: + parent.remove(widget) + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, spacing=0) + label = Gtk.Label() + label.set_use_markup(True) + label.set_markup('<b><i><big>' + name + '</big></i></b>') + label.set_alignment(0.00, 0.50) + label.set_padding(10, 10) + vbox.pack_start(label, False, True, 0) + sep = Gtk.HSeparator() + vbox.pack_start(sep, False, True, 0) + widget.set_margin_top(7) + widget.set_vexpand(True) + widget.set_hexpand(True) + vbox.pack_start(widget, True, True, 0) + scrolled = Gtk.ScrolledWindow() + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.NONE) + viewport.add(vbox) + scrolled.add(viewport) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.show_all() + # Add this page to the notebook + index = self.notebook.append_page(scrolled, None) + self.liststore.append([index, name, _(name)]) + return name + + def remove_page(self, name): + """Removes a page from the notebook""" + self.page_num_to_remove = None + self.iter_to_remove = None + + def on_foreach_row(model, path, _iter, user_data): + row_name = deluge.common.decode_bytes(model.get_value(_iter, 1)) + if row_name == user_data: + # This is the row we need to remove + self.page_num_to_remove = model.get_value(_iter, 0) + self.iter_to_remove = _iter + # Return True to stop foreach iterating + return True + + self.liststore.foreach(on_foreach_row, name) + + # Remove the page and row + if self.page_num_to_remove is not None: + self.notebook.remove_page(self.page_num_to_remove) + if self.iter_to_remove is not None: + self.liststore.remove(self.iter_to_remove) + + # We need to re-adjust the index values for the remaining pages + for idx, __ in enumerate(self.liststore): + self.liststore[idx][0] = idx + + def show(self, page=None): + """Page should be the string in the left list.. ie, 'Network' or + 'Bandwidth'""" + self.window_open = True + if page is not None: + for index, string, __ in self.liststore: + if page == string: + self.treeview.get_selection().select_path(index) + break + + 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 + ) + + def on_get_available_plugins(plugins): + self.all_plugins = plugins + client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins) + + def on_get_enabled_plugins(plugins): + self.enabled_plugins = plugins + client.core.get_listen_port().addCallback(on_get_listen_port) + + def on_get_listen_port(port): + self.active_port = port + client.core.get_session_status(DISK_CACHE_KEYS).addCallback( + on_get_session_status + ) + + def on_get_session_status(status): + self.cache_status = status + self._show() + + # This starts a series of client.core requests prior to showing the window + client.core.get_config().addCallback(on_get_config) + else: + self._show() + + def start(self): + if self.window_open: + self.show() + + def stop(self): + self.core_config = None + if self.window_open: + self._show() + + def _show(self): + self.is_connected = self.core_config != {} and self.core_config is not None + core_widgets = { + 'chk_move_completed': ('active', 'move_completed'), + 'chk_copy_torrent_file': ('active', 'copy_torrent_file'), + 'chk_del_copy_torrent_file': ('active', 'del_copy_torrent_file'), + 'chk_pre_allocation': ('active', 'pre_allocate_storage'), + 'chk_prioritize_first_last_pieces': ( + 'active', + 'prioritize_first_last_pieces', + ), + 'chk_sequential_download': ('active', 'sequential_download'), + 'chk_add_paused': ('active', 'add_paused'), + 'active_port_label': ('text', lambda: str(self.active_port)), + 'spin_incoming_port': ( + 'value', + lambda: self.core_config['listen_ports'][0], + ), + 'chk_random_incoming_port': ('active', 'random_port'), + 'spin_outgoing_port_min': ( + 'value', + lambda: self.core_config['outgoing_ports'][0], + ), + 'spin_outgoing_port_max': ( + 'value', + lambda: self.core_config['outgoing_ports'][1], + ), + 'chk_random_outgoing_ports': ('active', 'random_outgoing_ports'), + 'entry_interface': ('text', 'listen_interface'), + 'entry_outgoing_interface': ('text', 'outgoing_interface'), + 'entry_peer_tos': ('text', 'peer_tos'), + 'chk_dht': ('active', 'dht'), + 'chk_upnp': ('active', 'upnp'), + 'chk_natpmp': ('active', 'natpmp'), + 'chk_utpex': ('active', 'utpex'), + 'chk_lsd': ('active', 'lsd'), + 'chk_new_releases': ('active', 'new_release_check'), + 'chk_send_info': ('active', 'send_info'), + 'entry_geoip': ('text', 'geoip_db_location'), + 'combo_encin': ('active', 'enc_in_policy'), + 'combo_encout': ('active', 'enc_out_policy'), + 'combo_enclevel': ('active', 'enc_level'), + 'spin_max_connections_global': ('value', 'max_connections_global'), + 'spin_max_download': ('value', 'max_download_speed'), + 'spin_max_upload': ('value', 'max_upload_speed'), + 'spin_max_upload_slots_global': ('value', 'max_upload_slots_global'), + 'spin_max_half_open_connections': ('value', 'max_connections_per_second'), + 'spin_max_connections_per_second': ('value', 'max_connections_per_second'), + 'chk_ignore_limits_on_local_network': ( + 'active', + 'ignore_limits_on_local_network', + ), + 'chk_rate_limit_ip_overhead': ('active', 'rate_limit_ip_overhead'), + 'spin_max_connections_per_torrent': ( + 'value', + 'max_connections_per_torrent', + ), + 'spin_max_upload_slots_per_torrent': ( + 'value', + 'max_upload_slots_per_torrent', + ), + 'spin_max_download_per_torrent': ( + 'value', + 'max_download_speed_per_torrent', + ), + 'spin_max_upload_per_torrent': ('value', 'max_upload_speed_per_torrent'), + 'spin_daemon_port': ('value', 'daemon_port'), + 'chk_allow_remote_connections': ('active', 'allow_remote'), + 'spin_active': ('value', 'max_active_limit'), + 'spin_seeding': ('value', 'max_active_seeding'), + 'spin_downloading': ('value', 'max_active_downloading'), + 'chk_dont_count_slow_torrents': ('active', 'dont_count_slow_torrents'), + 'chk_auto_manage_prefer_seeds': ('active', 'auto_manage_prefer_seeds'), + 'chk_queue_new_top': ('active', 'queue_new_to_top'), + 'spin_share_ratio_limit': ('value', 'share_ratio_limit'), + 'spin_seed_time_ratio_limit': ('value', 'seed_time_ratio_limit'), + 'spin_seed_time_limit': ('value', 'seed_time_limit'), + 'chk_share_ratio': ('active', 'stop_seed_at_ratio'), + 'spin_share_ratio': ('value', 'stop_seed_ratio'), + 'radio_pause_ratio': ('active', 'stop_seed_at_ratio'), + 'radio_remove_ratio': ('active', 'remove_seed_at_ratio'), + 'spin_cache_size': ('value', 'cache_size'), + 'spin_cache_expiry': ('value', 'cache_expiry'), + 'combo_proxy_type': ('active', lambda: self.core_config['proxy']['type']), + 'entry_proxy_user': ('text', lambda: self.core_config['proxy']['username']), + 'entry_proxy_pass': ('text', lambda: self.core_config['proxy']['password']), + 'entry_proxy_host': ('text', lambda: self.core_config['proxy']['hostname']), + 'spin_proxy_port': ('value', lambda: self.core_config['proxy']['port']), + 'chk_proxy_host_resolve': ( + 'active', + lambda: self.core_config['proxy']['proxy_hostnames'], + ), + 'chk_proxy_peer_conn': ( + 'active', + lambda: self.core_config['proxy']['proxy_peer_connections'], + ), + 'chk_proxy_tracker_conn': ( + 'active', + lambda: self.core_config['proxy']['proxy_tracker_connections'], + ), + 'chk_force_proxy': ( + 'active', + lambda: self.core_config['proxy']['force_proxy'], + ), + 'chk_anonymous_mode': ( + 'active', + lambda: self.core_config['proxy']['anonymous_mode'], + ), + 'accounts_add': (None, None), + 'accounts_listview': (None, None), + 'button_cache_refresh': (None, None), + 'button_plugin_install': (None, None), + 'button_rescan_plugins': (None, None), + 'button_find_plugins': (None, None), + 'button_testport': (None, None), + 'plugin_listview': (None, None), + } + + core_widgets[self.download_location_path_chooser] = ( + 'path_chooser', + 'download_location', + ) + core_widgets[self.move_completed_path_chooser] = ( + 'path_chooser', + 'move_completed_path', + ) + core_widgets[self.copy_torrent_files_path_chooser] = ( + 'path_chooser', + 'torrentfiles_location', + ) + + # Update the widgets accordingly + for key in core_widgets: + modifier = core_widgets[key][0] + try: + widget = self.builder.get_object(key) + except TypeError: + widget = key + + widget.set_sensitive(self.is_connected) + + if self.is_connected: + value = core_widgets[key][1] + try: + value = self.core_config[value] + except KeyError: + if callable(value): + value = value() + elif modifier: + value = { + 'active': False, + 'not_active': False, + 'value': 0, + 'text': '', + 'path_chooser': '', + }[modifier] + + if modifier == 'active': + widget.set_active(value) + elif modifier == 'not_active': + widget.set_active(not value) + elif modifier == 'value': + widget.set_value(float(value)) + elif modifier == 'text': + if value is None: + value = '' + widget.set_text(value) + elif modifier == 'path_chooser': + widget.set_text(value, cursor_end=False, default_text=True) + + if self.is_connected: + for key in core_widgets: + try: + widget = self.builder.get_object(key) + except TypeError: + widget = key + # Update the toggle status if necessary + self.on_toggle(widget) + + # Downloads tab # + self.builder.get_object('chk_show_dialog').set_active( + self.gtkui_config['interactive_add'] + ) + self.builder.get_object('chk_focus_dialog').set_active( + self.gtkui_config['focus_add_dialog'] + ) + + # Interface tab # + self.builder.get_object('chk_use_tray').set_active( + self.gtkui_config['enable_system_tray'] + ) + self.builder.get_object('chk_min_on_close').set_active( + self.gtkui_config['close_to_tray'] + ) + self.builder.get_object('chk_start_in_tray').set_active( + self.gtkui_config['start_in_tray'] + ) + self.builder.get_object('radio_appind').set_active( + self.gtkui_config['enable_appindicator'] + ) + self.builder.get_object('chk_lock_tray').set_active( + self.gtkui_config['lock_tray'] + ) + self.builder.get_object('radio_standalone').set_active( + self.gtkui_config['standalone'] + ) + self.builder.get_object('radio_thinclient').set_active( + not self.gtkui_config['standalone'] + ) + self.builder.get_object('chk_prefer_dark_theme').set_active( + self.gtkui_config['prefer_dark_theme'] + ) + self.builder.get_object('chk_show_rate_in_title').set_active( + self.gtkui_config['show_rate_in_title'] + ) + self.builder.get_object('chk_focus_main_window_on_add').set_active( + self.gtkui_config['focus_main_window_on_add'] + ) + self.builder.get_object('piecesbar_toggle').set_active( + self.gtkui_config['show_piecesbar'] + ) + self.builder.get_object('urldetect_toggle').set_active( + self.gtkui_config['detect_urls'] + ) + self.__set_color('completed', from_config=True) + self.__set_color('downloading', from_config=True) + self.__set_color('waiting', from_config=True) + self.__set_color('missing', from_config=True) + + # Other tab # + self.builder.get_object('chk_show_new_releases').set_active( + self.gtkui_config['show_new_releases'] + ) + + # Cache tab # + if client.connected(): + self.__update_cache_status() + + # Plugins tab # + all_plugins = self.all_plugins + enabled_plugins = self.enabled_plugins + # Clear the existing list so we don't duplicate entries. + self.plugin_liststore.clear() + # Iterate through the lists and add them to the liststore + for plugin in all_plugins: + enabled = plugin in enabled_plugins + row = self.plugin_liststore.append() + self.plugin_liststore.set_value(row, 0, plugin) + self.plugin_liststore.set_value(row, 1, enabled) + self.plugin_liststore.set_value(row, 2, _(plugin)) + + # Now show the dialog + self.pref_dialog.show() + + def set_config(self, hide=False): + """ + Sets all altered config values in the core. + + :param hide: bool, if True, will not re-show the dialog and will hide it instead + """ + + # Get the values from the dialog + new_core_config = {} + new_gtkui_config = {} + + # Downloads tab # + new_gtkui_config['interactive_add'] = self.builder.get_object( + 'chk_show_dialog' + ).get_active() + new_gtkui_config['focus_add_dialog'] = self.builder.get_object( + 'chk_focus_dialog' + ).get_active() + + for state in ('missing', 'waiting', 'downloading', 'completed'): + color = self.builder.get_object('%s_color' % state).get_color() + new_gtkui_config['pieces_color_%s' % state] = [ + color.red, + color.green, + color.blue, + ] + + new_core_config['copy_torrent_file'] = self.builder.get_object( + 'chk_copy_torrent_file' + ).get_active() + new_core_config['del_copy_torrent_file'] = self.builder.get_object( + 'chk_del_copy_torrent_file' + ).get_active() + new_core_config['move_completed'] = self.builder.get_object( + 'chk_move_completed' + ).get_active() + + new_core_config[ + 'download_location' + ] = self.download_location_path_chooser.get_text() + new_core_config[ + 'move_completed_path' + ] = self.move_completed_path_chooser.get_text() + new_core_config[ + 'torrentfiles_location' + ] = self.copy_torrent_files_path_chooser.get_text() + new_core_config['prioritize_first_last_pieces'] = self.builder.get_object( + 'chk_prioritize_first_last_pieces' + ).get_active() + new_core_config['sequential_download'] = self.builder.get_object( + 'chk_sequential_download' + ).get_active() + new_core_config['add_paused'] = self.builder.get_object( + 'chk_add_paused' + ).get_active() + new_core_config['pre_allocate_storage'] = self.builder.get_object( + 'chk_pre_allocation' + ).get_active() + + # Network tab # + listen_ports = [ + self.builder.get_object('spin_incoming_port').get_value_as_int() + ] * 2 + new_core_config['listen_ports'] = listen_ports + new_core_config['random_port'] = self.builder.get_object( + 'chk_random_incoming_port' + ).get_active() + outgoing_ports = ( + self.builder.get_object('spin_outgoing_port_min').get_value_as_int(), + self.builder.get_object('spin_outgoing_port_max').get_value_as_int(), + ) + new_core_config['outgoing_ports'] = outgoing_ports + new_core_config['random_outgoing_ports'] = self.builder.get_object( + 'chk_random_outgoing_ports' + ).get_active() + incoming_address = self.builder.get_object('entry_interface').get_text().strip() + if deluge.common.is_interface(incoming_address) or not incoming_address: + new_core_config['listen_interface'] = incoming_address + outgoing_address = ( + self.builder.get_object('entry_outgoing_interface').get_text().strip() + ) + if deluge.common.is_interface(outgoing_address) or not outgoing_address: + new_core_config['outgoing_interface'] = ( + self.builder.get_object('entry_outgoing_interface').get_text().strip() + ) + new_core_config['peer_tos'] = self.builder.get_object( + 'entry_peer_tos' + ).get_text() + new_core_config['dht'] = self.builder.get_object('chk_dht').get_active() + new_core_config['upnp'] = self.builder.get_object('chk_upnp').get_active() + new_core_config['natpmp'] = self.builder.get_object('chk_natpmp').get_active() + new_core_config['utpex'] = self.builder.get_object('chk_utpex').get_active() + new_core_config['lsd'] = self.builder.get_object('chk_lsd').get_active() + new_core_config['enc_in_policy'] = self.builder.get_object( + 'combo_encin' + ).get_active() + new_core_config['enc_out_policy'] = self.builder.get_object( + 'combo_encout' + ).get_active() + new_core_config['enc_level'] = self.builder.get_object( + 'combo_enclevel' + ).get_active() + + # Bandwidth tab # + new_core_config['max_connections_global'] = self.builder.get_object( + 'spin_max_connections_global' + ).get_value_as_int() + new_core_config['max_download_speed'] = self.builder.get_object( + 'spin_max_download' + ).get_value() + new_core_config['max_upload_speed'] = self.builder.get_object( + 'spin_max_upload' + ).get_value() + new_core_config['max_upload_slots_global'] = self.builder.get_object( + 'spin_max_upload_slots_global' + ).get_value_as_int() + new_core_config['max_half_open_connections'] = self.builder.get_object( + 'spin_max_half_open_connections' + ).get_value_as_int() + new_core_config['max_connections_per_second'] = self.builder.get_object( + 'spin_max_connections_per_second' + ).get_value_as_int() + new_core_config['max_connections_per_torrent'] = self.builder.get_object( + 'spin_max_connections_per_torrent' + ).get_value_as_int() + new_core_config['max_upload_slots_per_torrent'] = self.builder.get_object( + 'spin_max_upload_slots_per_torrent' + ).get_value_as_int() + new_core_config['max_upload_speed_per_torrent'] = self.builder.get_object( + 'spin_max_upload_per_torrent' + ).get_value() + new_core_config['max_download_speed_per_torrent'] = self.builder.get_object( + 'spin_max_download_per_torrent' + ).get_value() + new_core_config['ignore_limits_on_local_network'] = self.builder.get_object( + 'chk_ignore_limits_on_local_network' + ).get_active() + new_core_config['rate_limit_ip_overhead'] = self.builder.get_object( + 'chk_rate_limit_ip_overhead' + ).get_active() + + # Interface tab # + new_gtkui_config['prefer_dark_theme'] = self.builder.get_object( + 'chk_prefer_dark_theme' + ).get_active() + new_gtkui_config['enable_system_tray'] = self.builder.get_object( + 'chk_use_tray' + ).get_active() + new_gtkui_config['close_to_tray'] = self.builder.get_object( + 'chk_min_on_close' + ).get_active() + new_gtkui_config['start_in_tray'] = self.builder.get_object( + 'chk_start_in_tray' + ).get_active() + new_gtkui_config['enable_appindicator'] = self.builder.get_object( + 'radio_appind' + ).get_active() + new_gtkui_config['lock_tray'] = self.builder.get_object( + 'chk_lock_tray' + ).get_active() + passhex = sha( + deluge.common.decode_bytes( + self.builder.get_object('txt_tray_password').get_text() + ).encode() + ).hexdigest() + if passhex != 'c07eb5a8c0dc7bb81c217b67f11c3b7a5e95ffd7': + new_gtkui_config['tray_password'] = passhex + + was_standalone = self.gtkui_config['standalone'] + new_gtkui_standalone = self.builder.get_object('radio_standalone').get_active() + new_gtkui_config['standalone'] = new_gtkui_standalone + + new_gtkui_config['show_rate_in_title'] = self.builder.get_object( + 'chk_show_rate_in_title' + ).get_active() + new_gtkui_config['focus_main_window_on_add'] = self.builder.get_object( + 'chk_focus_main_window_on_add' + ).get_active() + + # Other tab # + new_gtkui_config['show_new_releases'] = self.builder.get_object( + 'chk_show_new_releases' + ).get_active() + new_core_config['send_info'] = self.builder.get_object( + 'chk_send_info' + ).get_active() + new_core_config['geoip_db_location'] = self.builder.get_object( + 'entry_geoip' + ).get_text() + + # Daemon tab # + new_core_config['daemon_port'] = self.builder.get_object( + 'spin_daemon_port' + ).get_value_as_int() + new_core_config['allow_remote'] = self.builder.get_object( + 'chk_allow_remote_connections' + ).get_active() + new_core_config['new_release_check'] = self.builder.get_object( + 'chk_new_releases' + ).get_active() + + # Proxy tab # + new_core_config['proxy'] = { + 'type': self.builder.get_object('combo_proxy_type').get_active(), + 'username': self.builder.get_object('entry_proxy_user').get_text(), + 'password': self.builder.get_object('entry_proxy_pass').get_text(), + 'hostname': self.builder.get_object('entry_proxy_host').get_text(), + 'port': self.builder.get_object('spin_proxy_port').get_value_as_int(), + 'proxy_hostnames': self.builder.get_object( + 'chk_proxy_host_resolve' + ).get_active(), + 'proxy_peer_connections': self.builder.get_object( + 'chk_proxy_peer_conn' + ).get_active(), + 'proxy_tracker_connections': self.builder.get_object( + 'chk_proxy_tracker_conn' + ).get_active(), + 'force_proxy': self.builder.get_object('chk_force_proxy').get_active(), + 'anonymous_mode': self.builder.get_object( + 'chk_anonymous_mode' + ).get_active(), + } + + # Queue tab # + new_core_config['queue_new_to_top'] = self.builder.get_object( + 'chk_queue_new_top' + ).get_active() + new_core_config['max_active_seeding'] = self.builder.get_object( + 'spin_seeding' + ).get_value_as_int() + new_core_config['max_active_downloading'] = self.builder.get_object( + 'spin_downloading' + ).get_value_as_int() + new_core_config['max_active_limit'] = self.builder.get_object( + 'spin_active' + ).get_value_as_int() + new_core_config['dont_count_slow_torrents'] = self.builder.get_object( + 'chk_dont_count_slow_torrents' + ).get_active() + new_core_config['auto_manage_prefer_seeds'] = self.builder.get_object( + 'chk_auto_manage_prefer_seeds' + ).get_active() + new_core_config['stop_seed_at_ratio'] = self.builder.get_object( + 'chk_share_ratio' + ).get_active() + new_core_config['remove_seed_at_ratio'] = self.builder.get_object( + 'radio_remove_ratio' + ).get_active() + new_core_config['stop_seed_ratio'] = self.builder.get_object( + 'spin_share_ratio' + ).get_value() + new_core_config['share_ratio_limit'] = self.builder.get_object( + 'spin_share_ratio_limit' + ).get_value() + new_core_config['seed_time_ratio_limit'] = self.builder.get_object( + 'spin_seed_time_ratio_limit' + ).get_value() + new_core_config['seed_time_limit'] = self.builder.get_object( + 'spin_seed_time_limit' + ).get_value() + + # Cache tab # + new_core_config['cache_size'] = self.builder.get_object( + 'spin_cache_size' + ).get_value_as_int() + new_core_config['cache_expiry'] = self.builder.get_object( + 'spin_cache_expiry' + ).get_value_as_int() + + # Run plugin hook to apply preferences + component.get('PluginManager').run_on_apply_prefs() + + # Language + if self.language_checkbox.get_active(): + new_gtkui_config['language'] = None + else: + active = self.language_combo.get_active() + if active == -1: + dialog = InformationDialog( + _('Attention'), _('You must choose a language') + ) + dialog.run() + return + else: + model = self.language_combo.get_model() + new_gtkui_config['language'] = model.get(model.get_iter(active), 0)[0] + + if new_gtkui_config['language'] != self.gtkui_config['language']: + dialog = InformationDialog( + _('Attention'), + _('You must now restart the deluge UI for the changes to take effect.'), + ) + dialog.run() + + # GtkUI + for key in new_gtkui_config: + # The values do not match so this needs to be updated + if self.gtkui_config[key] != new_gtkui_config[key]: + self.gtkui_config[key] = new_gtkui_config[key] + + # Core + if client.connected(): + # Only do this if we're connected to a daemon + config_to_set = {} + for key in new_core_config: + # The values do not match so this needs to be updated + if self.core_config[key] != new_core_config[key]: + config_to_set[key] = new_core_config[key] + + if config_to_set: + # Set each changed config value in the core + client.core.set_config(config_to_set) + client.force_call(True) + # Update the configuration + self.core_config.update(config_to_set) + + if hide: + self.hide() + else: + # Re-show the dialog to make sure everything has been updated + self.show() + + if was_standalone != new_gtkui_standalone: + + def on_response(response): + if response == Gtk.ResponseType.YES: + shutdown_daemon = ( + not client.is_standalone() + and client.connected() + and client.is_localhost() + ) + component.get('MainWindow').quit( + shutdown=shutdown_daemon, restart=True + ) + else: + self.gtkui_config['standalone'] = not new_gtkui_standalone + self.builder.get_object('radio_standalone').set_active( + self.gtkui_config['standalone'] + ) + self.builder.get_object('radio_thinclient').set_active( + not self.gtkui_config['standalone'] + ) + + mode = _('Thinclient') if was_standalone else _('Standalone') + dialog = YesNoDialog( + _('Switching Deluge Client Mode...'), + _('Do you want to restart to use %s mode?' % mode), + ) + dialog.run().addCallback(on_response) + + def hide(self): + self.window_open = False + self.builder.get_object('port_spinner').stop() + self.builder.get_object('port_img').hide() + self.pref_dialog.hide() + + def __update_cache_status(self): + # Updates the cache status labels with the info in the dict + cache_labels = ( + 'label_cache_read_ops', + 'label_cache_write_ops', + 'label_cache_num_blocks_read', + 'label_cache_num_blocks_written', + 'label_cache_read_hit_ratio', + 'label_cache_write_hit_ratio', + 'label_cache_disk_blocks_in_use', + 'label_cache_read_cache_blocks', + ) + + for widget_name in cache_labels: + widget = self.builder.get_object(widget_name) + key = widget_name[len('label_cache_') :] + if not widget_name.endswith('ratio'): + key = 'disk.' + key + value = self.cache_status.get(key, 0) + if isinstance(value, float): + value = '%.2f' % value + else: + value = str(value) + + widget.set_text(value) + + def on_button_cache_refresh_clicked(self, widget): + def on_get_session_status(status): + self.cache_status = status + self.__update_cache_status() + + client.core.get_session_status(DISK_CACHE_KEYS).addCallback( + on_get_session_status + ) + + def on_pref_dialog_delete_event(self, widget, event): + self.hide() + return True + + def load_pref_dialog_state(self): + w = self.gtkui_config['pref_dialog_width'] + h = self.gtkui_config['pref_dialog_height'] + if w is not None and h is not None: + self.pref_dialog.resize(w, h) + + def on_pref_dialog_configure_event(self, widget, event): + self.gtkui_config['pref_dialog_width'] = event.width + self.gtkui_config['pref_dialog_height'] = event.height + + def on_toggle(self, widget): + """Handles widget sensitivity based on radio/check button values.""" + try: + value = widget.get_active() + except Exception: + return + + path_choosers = { + 'download_location_path_chooser': self.download_location_path_chooser, + 'move_completed_path_chooser': self.move_completed_path_chooser, + 'torrentfiles_location_path_chooser': self.copy_torrent_files_path_chooser, + } + + dependents = { + 'chk_show_dialog': {'chk_focus_dialog': True}, + 'chk_random_incoming_port': {'spin_incoming_port': False}, + 'chk_random_outgoing_ports': { + 'spin_outgoing_port_min': False, + 'spin_outgoing_port_max': False, + }, + 'chk_use_tray': { + 'radio_appind': True, + 'radio_systray': True, + 'chk_min_on_close': True, + 'chk_start_in_tray': True, + 'alignment_tray_type': True, + 'chk_lock_tray': True, + }, + 'chk_lock_tray': {'txt_tray_password': True, 'password_label': True}, + 'radio_open_folder_custom': { + 'combo_file_manager': False, + 'txt_open_folder_location': True, + }, + 'chk_move_completed': {'move_completed_path_chooser': True}, + 'chk_copy_torrent_file': { + 'torrentfiles_location_path_chooser': True, + 'chk_del_copy_torrent_file': True, + }, + 'chk_share_ratio': { + 'spin_share_ratio': True, + 'radio_pause_ratio': True, + 'radio_remove_ratio': True, + }, + } + + def update_dependent_widgets(name, value): + dependency = dependents[name] + for dep in dependency: + if dep in path_choosers: + depwidget = path_choosers[dep] + else: + depwidget = self.builder.get_object(dep) + sensitive = [not value, value][dependency[dep]] + depwidget.set_sensitive(sensitive) + if dep in dependents: + update_dependent_widgets(dep, depwidget.get_active() and sensitive) + + for key in dependents: + if widget != self.builder.get_object(key): + continue + update_dependent_widgets(key, value) + + def on_button_ok_clicked(self, data): + log.debug('on_button_ok_clicked') + self.set_config(hide=True) + return True + + def on_button_apply_clicked(self, data): + log.debug('on_button_apply_clicked') + self.set_config() + + def on_button_cancel_clicked(self, data): + log.debug('on_button_cancel_clicked') + Gtk.Settings.get_default().set_property( + 'gtk-application-prefer-dark-theme', + self.gtkui_config['prefer_dark_theme'], + ) + self.hide() + return True + + def on_selection_changed(self, treeselection): + # 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 + if client.connected(): + self._get_accounts_tab_data() + self.notebook.set_current_page(model.get_value(row, 0)) + except TypeError: + pass + + def on_test_port_clicked(self, data): + log.debug('on_test_port_clicked') + + def on_get_test(status): + self.builder.get_object('port_spinner').stop() + self.builder.get_object('port_spinner').hide() + if status: + self.builder.get_object('port_img').set_from_icon_name( + 'emblem-ok-symbolic', Gtk.IconSize.MENU + ) + self.builder.get_object('port_img').show() + else: + self.builder.get_object('port_img').set_from_icon_name( + 'dialog-warning-symbolic', Gtk.IconSize.MENU + ) + self.builder.get_object('port_img').show() + + client.core.test_listen_port().addCallback(on_get_test) + self.builder.get_object('port_spinner').start() + self.builder.get_object('port_spinner').show() + self.builder.get_object('port_img').hide() + client.force_call() + + def on_plugin_toggled(self, renderer, path): + row = self.plugin_liststore.get_iter_from_string(path) + name = self.plugin_liststore.get_value(row, 0) + value = self.plugin_liststore.get_value(row, 1) + log.debug('on_plugin_toggled - %s: %s', name, value) + self.plugin_liststore.set_value(row, 1, not value) + if not value: + d = client.core.enable_plugin(name) + else: + d = client.core.disable_plugin(name) + + def on_plugin_action(arg): + if not value and arg is False: + log.warning('Failed to enable plugin: %s', name) + self.plugin_liststore.set_value(row, 1, False) + + d.addBoth(on_plugin_action) + + def on_plugin_selection_changed(self, treeselection): + log.debug('on_plugin_selection_changed') + (model, itr) = treeselection.get_selected() + if not itr: + return + name = model[itr][0] + plugin_info = component.get('PluginManager').get_plugin_info(name) + self.builder.get_object('label_plugin_author').set_text(plugin_info['Author']) + self.builder.get_object('label_plugin_version').set_text(plugin_info['Version']) + self.builder.get_object('label_plugin_email').set_text( + plugin_info['Author-email'] + ) + self.builder.get_object('label_plugin_homepage').set_text( + plugin_info['Home-page'] + ) + self.builder.get_object('label_plugin_details').set_text( + plugin_info['Description'] + ) + + def on_button_plugin_install_clicked(self, widget): + log.debug('on_button_plugin_install_clicked') + chooser = Gtk.FileChooserDialog( + _('Select the Plugin'), + self.pref_dialog, + Gtk.FileChooserAction.OPEN, + buttons=( + _('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_Open'), + Gtk.ResponseType.OK, + ), + ) + + chooser.set_transient_for(self.pref_dialog) + chooser.set_select_multiple(False) + chooser.set_property('skip-taskbar-hint', True) + + file_filter = Gtk.FileFilter() + file_filter.set_name(_('Plugin Eggs')) + file_filter.add_pattern('*.' + 'egg') + chooser.add_filter(file_filter) + + # Run the dialog + response = chooser.run() + + if response == Gtk.ResponseType.OK: + filepath = deluge.common.decode_bytes(chooser.get_filename()) + else: + chooser.destroy() + return + + import shutil + from base64 import b64encode + + filename = os.path.split(filepath)[1] + shutil.copyfile(filepath, os.path.join(get_config_dir(), 'plugins', filename)) + + component.get('PluginManager').scan_for_plugins() + + if not client.is_localhost(): + # We need to send this plugin to the daemon + with open(filepath, 'rb') as _file: + filedump = b64encode(_file.read()) + client.core.upload_plugin(filename, filedump) + + client.core.rescan_plugins() + chooser.destroy() + # We need to re-show the preferences dialog to show the new plugins + self.show() + + def on_button_rescan_plugins_clicked(self, widget): + component.get('PluginManager').scan_for_plugins() + if client.connected(): + client.core.rescan_plugins() + self.show() + + def on_button_find_plugins_clicked(self, widget): + deluge.common.open_url_in_browser('http://dev.deluge-torrent.org/wiki/Plugins') + + def on_combo_encryption_changed(self, widget): + combo_encin = self.builder.get_object('combo_encin').get_active() + combo_encout = self.builder.get_object('combo_encout').get_active() + combo_enclevel = self.builder.get_object('combo_enclevel') + + # If incoming and outgoing both set to disabled, disable level combobox + if combo_encin == 2 and combo_encout == 2: + combo_enclevel.set_sensitive(False) + elif self.is_connected: + combo_enclevel.set_sensitive(True) + + def on_combo_proxy_type_changed(self, widget): + proxy_type = self.builder.get_object('combo_proxy_type').get_active() + proxy_entries = [ + 'label_proxy_host', + 'entry_proxy_host', + 'label_proxy_port', + 'spin_proxy_port', + 'label_proxy_pass', + 'entry_proxy_pass', + 'label_proxy_user', + 'entry_proxy_user', + 'chk_proxy_host_resolve', + 'chk_proxy_peer_conn', + 'chk_proxy_tracker_conn', + ] + + # 0: None, 1: Socks4, 2: Socks5, 3: Socks5 Auth, 4: HTTP, 5: HTTP Auth, 6: I2P + show_entries = [] + if proxy_type > 0: + show_entries.extend( + [ + 'label_proxy_host', + 'entry_proxy_host', + 'label_proxy_port', + 'spin_proxy_port', + 'chk_proxy_peer_conn', + 'chk_proxy_tracker_conn', + ] + ) + if proxy_type in (3, 5): + show_entries.extend( + [ + 'label_proxy_pass', + 'entry_proxy_pass', + 'label_proxy_user', + 'entry_proxy_user', + ] + ) + if proxy_type in (2, 3, 4, 5): + show_entries.extend(['chk_proxy_host_resolve']) + + for entry in proxy_entries: + if entry in show_entries: + self.builder.get_object(entry).show() + else: + self.builder.get_object(entry).hide() + + def on_entry_proxy_host_paste_clipboard(self, widget): + text = get_clipboard_text() + log.debug('on_entry_proxy_host_paste-clipboard: got paste: %s', text) + text = text if '//' in text else '//' + text + parsed = urlparse(text) + if parsed.hostname: + widget.set_text(parsed.hostname) + widget.emit_stop_by_name('paste-clipboard') + if parsed.port: + self.builder.get_object('spin_proxy_port').set_value(parsed.port) + if parsed.username: + self.builder.get_object('entry_proxy_user').set_text(parsed.username) + if parsed.password: + self.builder.get_object('entry_proxy_pass').set_text(parsed.password) + + def on_button_associate_magnet_clicked(self, widget): + 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.type == NotAuthorizedError: + self.accounts_frame.hide() + else: + ErrorDialog( + _('Server Side Error'), + _('An error occurred on the server'), + parent=self.pref_dialog, + details=failure.getErrorMessage(), + ).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().items(): + 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: + accounts_iter = self.accounts_liststore.append() + self.accounts_liststore.set_value( + accounts_iter, ACCOUNTS_USERNAME, account['username'] + ) + self.accounts_liststore.set_value( + accounts_iter, ACCOUNTS_LEVEL, account['authlevel'] + ) + self.accounts_liststore.set_value( + accounts_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 + level = model[itr][1] + if level: + self.builder.get_object('accounts_edit').set_sensitive(True) + self.builder.get_object('accounts_delete').set_sensitive(True) + else: + self.builder.get_object('accounts_edit').set_sensitive(False) + self.builder.get_object('accounts_delete').set_sensitive(False) + + @maybe_coroutine + async def on_accounts_add_clicked(self, widget): + dialog = AccountDialog( + levels_mapping=client.auth_levels_mapping, parent=self.pref_dialog + ) + response = await dialog.run() + if response != Gtk.ResponseType.OK: + return + + account = dialog.account + try: + await client.core.create_account(*account) + except AuthManagerError as ex: + return ErrorDialog( + _('Error Adding Account'), + _('Authentication failed'), + parent=self.pref_dialog, + details=ex, + ).run() + except Exception as ex: + return ErrorDialog( + _('Error Adding Account'), + _(f'An error occurred while adding account: {account}'), + parent=self.pref_dialog, + details=ex, + ).run() + + self.accounts_liststore.set( + self.accounts_liststore.append(), + [ACCOUNTS_USERNAME, ACCOUNTS_LEVEL, ACCOUNTS_PASSWORD], + [account.username, account.authlevel, account.password], + ) + + def on_accounts_edit_clicked(self, widget): + (model, itr) = self.accounts_listview.get_selection().get_selected() + if not itr: + return + + dialog = 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): + ErrorDialog( + _('Error Updating Account'), + _('An error occurred while updating account'), + parent=self.pref_dialog, + details=failure.getErrorMessage(), + ).run() + + if response_id == Gtk.ResponseType.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 want to remove the account with the ' + 'username "%(username)s"?' % {'username': username} + ) + dialog = 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.type == AuthManagerError: + ErrorDialog( + _('Error Removing Account'), + _('Auhentication failed'), + parent=self.pref_dialog, + details=failure.getErrorMessage(), + ).run() + else: + ErrorDialog( + _('Error Removing Account'), + _('An error occurred while removing account'), + parent=self.pref_dialog, + details=failure.getErrorMessage(), + ).run() + + if response_id == Gtk.ResponseType.YES: + client.core.remove_account(username).addCallback(remove_ok).addErrback( + remove_fail + ) + + dialog.run().addCallback(dialog_finished) + + def on_piecesbar_toggle_toggled(self, widget): + self.gtkui_config['show_piecesbar'] = widget.get_active() + colors_widget = self.builder.get_object('piecebar_colors_expander') + colors_widget.set_visible(widget.get_active()) + + def on_urldetect_toggle_toggled(self, widget): + self.gtkui_config['detect_urls'] = widget.get_active() + + def on_checkbutton_language_toggled(self, widget): + self.language_combo.set_visible(not self.language_checkbox.get_active()) + + def on_completed_color_set(self, widget): + self.__set_color('completed') + + def on_revert_color_completed_clicked(self, widget): + self.__revert_color('completed') + + def on_downloading_color_set(self, widget): + self.__set_color('downloading') + + def on_revert_color_downloading_clicked(self, widget): + self.__revert_color('downloading') + + def on_waiting_color_set(self, widget): + self.__set_color('waiting') + + def on_revert_color_waiting_clicked(self, widget): + self.__revert_color('waiting') + + def on_missing_color_set(self, widget): + self.__set_color('missing') + + def on_revert_color_missing_clicked(self, widget): + self.__revert_color('missing') + + def __set_color(self, state, from_config=False): + if from_config: + color = Color(*self.gtkui_config['pieces_color_%s' % state]) + log.debug( + 'Setting %r color state from config to %s', + state, + (color.red, color.green, color.blue), + ) + self.builder.get_object('%s_color' % state).set_color(color) + else: + color = self.builder.get_object('%s_color' % state).get_color() + log.debug( + 'Setting %r color state to %s', + state, + (color.red, color.green, color.blue), + ) + self.gtkui_config['pieces_color_%s' % state] = [ + color.red, + color.green, + color.blue, + ] + self.gtkui_config.save() + self.gtkui_config.apply_set_functions('pieces_colors') + + self.builder.get_object('revert_color_%s' % state).set_sensitive( + [color.red, color.green, color.blue] != self.COLOR_DEFAULTS[state] + ) + + def __revert_color(self, state, from_config=False): + log.debug('Reverting %r color state', state) + self.builder.get_object('%s_color' % state).set_color( + Color(*self.COLOR_DEFAULTS[state]) + ) + self.builder.get_object('revert_color_%s' % state).set_sensitive(False) + self.gtkui_config.apply_set_functions('pieces_colors') diff --git a/deluge/ui/gtk3/queuedtorrents.py b/deluge/ui/gtk3/queuedtorrents.py new file mode 100644 index 0000000..6fdecec --- /dev/null +++ b/deluge/ui/gtk3/queuedtorrents.py @@ -0,0 +1,165 @@ +# +# Copyright (C) 2007 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. +# + +import logging +import os.path + +from gi.repository.GLib import timeout_add +from gi.repository.Gtk import Builder, CellRendererText, ListStore, TreeViewColumn + +import deluge.common +import deluge.component as component +from deluge.configmanager import ConfigManager + +from .common import get_logo +from .ipcinterface import process_args + +log = logging.getLogger(__name__) + + +class QueuedTorrents(component.Component): + def __init__(self): + component.Component.__init__( + self, 'QueuedTorrents', depend=['StatusBar', 'AddTorrentDialog'] + ) + self.queue = [] + self.status_item = None + + self.config = ConfigManager('gtk3ui.conf') + self.builder = Builder() + self.builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'queuedtorrents.ui') + ) + ) + self.builder.get_object('chk_autoadd').set_active(self.config['autoadd_queued']) + self.dialog = self.builder.get_object('queued_torrents_dialog') + self.dialog.set_icon(get_logo(32)) + + self.builder.connect_signals(self) + + self.treeview = self.builder.get_object('treeview') + self.treeview.append_column( + TreeViewColumn(_('Torrent'), CellRendererText(), text=0) + ) + + self.liststore = ListStore(str, str) + self.treeview.set_model(self.liststore) + self.treeview.set_tooltip_column(1) + + def run(self): + self.dialog.set_transient_for(component.get('MainWindow').window) + self.dialog.show() + + def start(self): + if len(self.queue) == 0: + return + + # Make sure status bar info is showing + self.update_status_bar() + + # We only want the add button sensitive if we're connected to a host + self.builder.get_object('button_add').set_sensitive(True) + + if self.config['autoadd_queued'] or self.config['standalone']: + self.on_button_add_clicked(None) + else: + self.run() + + def stop(self): + # We only want the add button sensitive if we're connected to a host + self.builder.get_object('button_add').set_sensitive(False) + self.update_status_bar() + + def add_to_queue(self, torrents): + """Adds the list of torrents to the queue""" + # Add to the queue while removing duplicates + self.queue = list(set(self.queue + torrents)) + + # Update the liststore + self.liststore.clear() + for torrent in self.queue: + if deluge.common.is_magnet(torrent): + magnet = deluge.common.get_magnet_info(torrent) + self.liststore.append([magnet['name'], torrent]) + else: + self.liststore.append([os.path.split(torrent)[1], torrent]) + + # Update the status bar + self.update_status_bar() + + def update_status_bar(self): + """Attempts to update status bar""" + # If there are no queued torrents.. remove statusbar widgets and return + if len(self.queue) == 0: + if self.status_item is not None: + component.get('StatusBar').remove_item(self.status_item) + self.status_item = None + return False + + try: + component.get('StatusBar') + except Exception: + # The statusbar hasn't been loaded yet, so we'll add a timer to + # update it later. + timeout_add(100, self.update_status_bar) + return False + + # Set the label text for statusbar + if len(self.queue) > 1: + label = str(len(self.queue)) + _(' Torrents Queued') + else: + label = str(len(self.queue)) + _(' Torrent Queued') + + # Add the statusbar items if needed, or just modify the label if they + # have already been added. + if self.status_item is None: + self.status_item = component.get('StatusBar').add_item( + icon='view-sort-descending', + text=label, + callback=self.on_statusbar_click, + ) + else: + self.status_item.set_text(label) + + # We return False so the timer stops + return False + + def on_statusbar_click(self, widget, event): + log.debug('on_statusbar_click') + self.run() + + def on_button_remove_clicked(self, widget): + selected = self.treeview.get_selection().get_selected()[1] + if selected is not None: + path = self.liststore.get_value(selected, 1) + self.liststore.remove(selected) + self.queue.remove(path) + self.update_status_bar() + + def on_button_clear_clicked(self, widget): + self.liststore.clear() + del self.queue[:] + self.update_status_bar() + + def on_button_close_clicked(self, widget): + self.dialog.hide() + + def on_button_add_clicked(self, widget): + # Add all the torrents in the liststore + def add_torrent(model, path, _iter, data): + torrent_path = deluge.common.decode_bytes(model.get_value(_iter, 1)) + process_args([torrent_path]) + + self.liststore.foreach(add_torrent, None) + del self.queue[:] + self.dialog.hide() + self.update_status_bar() + + def on_chk_autoadd_toggled(self, widget): + self.config['autoadd_queued'] = widget.get_active() diff --git a/deluge/ui/gtk3/removetorrentdialog.py b/deluge/ui/gtk3/removetorrentdialog.py new file mode 100644 index 0000000..06fca77 --- /dev/null +++ b/deluge/ui/gtk3/removetorrentdialog.py @@ -0,0 +1,90 @@ +# +# Copyright (C) 2007-2008 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. +# + +import logging +import os + +from gi.repository import Gtk + +import deluge.common +import deluge.component as component +from deluge.ui.client import client + +log = logging.getLogger(__name__) + + +class RemoveTorrentDialog: + """ + This class is used to create and show a Remove Torrent Dialog. + + :param torrent_ids: the torrent_ids to remove + :type torrent_ids: list of torrent_ids + + :raises TypeError: if `torrent_id` is not a sequence type + :raises ValueError: if `torrent_id` contains no torrent_ids or is None + + """ + + def __init__(self, torrent_ids, delete_files=False): + if not isinstance(torrent_ids, list) and not isinstance(torrent_ids, tuple): + raise TypeError('requires a list of torrent_ids') + + if not torrent_ids: + raise ValueError('requires a list of torrent_ids') + + self.__torrent_ids = torrent_ids + + self.builder = Gtk.Builder() + self.builder.add_from_file( + deluge.common.resource_filename( + __package__, os.path.join('glade', 'remove_torrent_dialog.ui') + ) + ) + + self.__dialog = self.builder.get_object('remove_torrent_dialog') + self.__dialog.set_transient_for(component.get('MainWindow').window) + + self.builder.connect_signals(self) + self.builder.get_object('delete_files').set_active(delete_files) + label_title = self.builder.get_object('label_title') + label_torrents = self.builder.get_object('label_torrents') + num_torrents = len(self.__torrent_ids) + if num_torrents == 1: + label_torrents.set_markup( + component.get('TorrentView').get_torrent_status(self.__torrent_ids[0])[ + 'name' + ] + ) + else: + label_title.set_markup(_('Remove the selected torrents?')) + label_torrents.set_markup(_('Total of %s torrents selected') % num_torrents) + + def on_delete_files_toggled(self, widget): + self.builder.get_object('warning_label').set_visible(widget.get_active()) + + def __remove_torrents(self, remove_data): + # Unselect all to avoid issues with the selection changed event + component.get('TorrentView').treeview.get_selection().unselect_all() + + def on_removed_finished(errors): + if errors: + log.info('Error(s) occured when trying to delete torrent(s).') + for t_id, e_msg in errors: + log.warning('Error removing torrent %s : %s', t_id, e_msg) + + d = client.core.remove_torrents(self.__torrent_ids, remove_data) + d.addCallback(on_removed_finished) + + def run(self): + """ + Shows the dialog and awaits for user input. The user can select to + remove the torrent(s) from the session with or without their data. + """ + if self.__dialog.run() == Gtk.ResponseType.OK: + self.__remove_torrents(self.builder.get_object('delete_files').get_active()) + self.__dialog.destroy() diff --git a/deluge/ui/gtk3/sidebar.py b/deluge/ui/gtk3/sidebar.py new file mode 100644 index 0000000..5a2b154 --- /dev/null +++ b/deluge/ui/gtk3/sidebar.py @@ -0,0 +1,70 @@ +# +# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2008 Martijn Voncken <mvoncken@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. +# + +import logging + +from gi.repository.Gtk import Label, PolicyType, ScrolledWindow + +import deluge.component as component +from deluge.configmanager import ConfigManager + +log = logging.getLogger(__name__) + + +class SideBar(component.Component): + """ + manages the sidebar-tabs. + purpose : plugins + """ + + def __init__(self): + component.Component.__init__(self, 'SideBar') + main_builder = component.get('MainWindow').get_builder() + self.notebook = main_builder.get_object('sidebar_notebook') + self.config = ConfigManager('gtk3ui.conf') + + # Tabs holds references to the Tab widgets by their name + self.tabs = {} + + # Hide if necessary + self.visible(self.config['show_sidebar']) + + def visible(self, visible): + self.notebook.show() if visible else self.notebook.hide() + self.config['show_sidebar'] = visible + + def add_tab(self, widget, tab_name, label): + """Adds a tab object to the notebook.""" + log.debug('add tab: %s', tab_name) + self.tabs[tab_name] = widget + scrolled = ScrolledWindow() + scrolled.set_policy(PolicyType.AUTOMATIC, PolicyType.AUTOMATIC) + scrolled.add(widget) + self.notebook.insert_page(scrolled, Label(label=label), -1) + scrolled.show_all() + + self.after_update() + + def remove_tab(self, tab_name): + """Removes a tab by name.""" + self.notebook.remove_page(self.notebook.page_num(self.tabs[tab_name])) + del self.tabs[tab_name] + + self.after_update() + + def after_update(self): + # If there are no tabs visible, then do not show the notebook + if len(self.tabs) == 0: + self.visible(False) + + # If there is 1 tab, hide the tab-headers + if len(self.tabs) == 1: + self.notebook.set_show_tabs(False) + else: + self.notebook.set_show_tabs(True) diff --git a/deluge/ui/gtk3/status_tab.py b/deluge/ui/gtk3/status_tab.py new file mode 100644 index 0000000..6a9010b --- /dev/null +++ b/deluge/ui/gtk3/status_tab.py @@ -0,0 +1,159 @@ +# +# Copyright (C) 2008 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. +# + +import logging + +import deluge.component as component +from deluge.common import decode_bytes, fpeer +from deluge.configmanager import ConfigManager + +from .piecesbar import PiecesBar +from .tab_data_funcs import ( + fdate_or_never, + fpcnt, + fratio, + fseed_rank_or_dash, + fspeed_max, + ftime_or_dash, + ftotal_sized, +) +from .torrentdetails import Tab, TabWidget + +log = logging.getLogger(__name__) + + +class StatusTab(Tab): + def __init__(self): + super().__init__('Status', 'status_tab', 'status_tab_label') + + self.config = ConfigManager('gtk3ui.conf') + + self.progressbar = self.main_builder.get_object('progressbar') + self.piecesbar = None + + self.add_tab_widget('summary_availability', fratio, ('distributed_copies',)) + self.add_tab_widget( + 'summary_total_downloaded', + ftotal_sized, + ('all_time_download', 'total_payload_download'), + ) + self.add_tab_widget( + 'summary_total_uploaded', + ftotal_sized, + ('total_uploaded', 'total_payload_upload'), + ) + self.add_tab_widget( + 'summary_download_speed', + fspeed_max, + ('download_payload_rate', 'max_download_speed'), + ) + self.add_tab_widget( + 'summary_upload_speed', + fspeed_max, + ('upload_payload_rate', 'max_upload_speed'), + ) + self.add_tab_widget('summary_seeds', fpeer, ('num_seeds', 'total_seeds')) + self.add_tab_widget('summary_peers', fpeer, ('num_peers', 'total_peers')) + self.add_tab_widget('summary_eta', ftime_or_dash, ('eta',)) + self.add_tab_widget('summary_share_ratio', fratio, ('ratio',)) + self.add_tab_widget('summary_active_time', ftime_or_dash, ('active_time',)) + self.add_tab_widget('summary_seed_time', ftime_or_dash, ('seeding_time',)) + self.add_tab_widget( + 'summary_seed_rank', fseed_rank_or_dash, ('seed_rank', 'seeding_time') + ) + self.add_tab_widget('progressbar', fpcnt, ('progress', 'state', 'message')) + self.add_tab_widget( + 'summary_last_seen_complete', fdate_or_never, ('last_seen_complete',) + ) + self.add_tab_widget( + 'summary_last_transfer', ftime_or_dash, ('time_since_transfer',) + ) + + self.config.register_set_function( + 'show_piecesbar', self.on_show_piecesbar_config_changed, apply_now=True + ) + + def update(self): + # Get the first selected torrent + selected = component.get('TorrentView').get_selected_torrent() + + if not selected: + # No torrent is selected in the torrentview + self.clear() + return + + # Get the torrent status + status_keys = self.status_keys + if self.config['show_piecesbar']: + status_keys.extend(['pieces', 'num_pieces']) + + component.get('SessionProxy').get_torrent_status( + selected, status_keys + ).addCallback(self._on_get_torrent_status) + + def _on_get_torrent_status(self, status): + # Check to see if we got valid data from the core + if not status: + return + + # Update all the label widgets + for widget in self.tab_widgets.values(): + txt = self.widget_status_as_fstr(widget, status) + if decode_bytes(widget[0].get_text()) != txt: + widget[0].set_text(txt) + + # Update progress bar separately as it's a special case (not a label). + fraction = status['progress'] / 100 + + if self.config['show_piecesbar']: + if self.piecesbar.get_fraction() != fraction: + self.piecesbar.set_fraction(fraction) + if ( + status['state'] != 'Checking' + and self.piecesbar.get_pieces() != status['pieces'] + ): + # Skip pieces assignment if checking torrent. + self.piecesbar.set_pieces(status['pieces'], status['num_pieces']) + self.piecesbar.update() + else: + if self.progressbar.get_fraction() != fraction: + self.progressbar.set_fraction(fraction) + + def on_show_piecesbar_config_changed(self, key, show): + if show: + self.show_piecesbar() + else: + self.hide_piecesbar() + + def show_piecesbar(self): + if self.piecesbar is None: + self.piecesbar = PiecesBar() + self.main_builder.get_object('status_progress_vbox').pack_start( + self.piecesbar, False, False, 0 + ) + self.tab_widgets['piecesbar'] = TabWidget( + self.piecesbar, fpcnt, ('progress', 'state', 'message') + ) + self.piecesbar.show() + self.progressbar.hide() + + def hide_piecesbar(self): + self.progressbar.show() + if self.piecesbar: + self.piecesbar.hide() + self.tab_widgets.pop('piecesbar', None) + self.piecesbar = None + + def clear(self): + for widget in self.tab_widgets.values(): + widget[0].set_text('') + + if self.config['show_piecesbar']: + self.piecesbar.clear() + else: + self.progressbar.set_fraction(0) diff --git a/deluge/ui/gtk3/statusbar.py b/deluge/ui/gtk3/statusbar.py new file mode 100644 index 0000000..0a2e800 --- /dev/null +++ b/deluge/ui/gtk3/statusbar.py @@ -0,0 +1,578 @@ +# +# Copyright (C) 2007-2008 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. +# + +import logging + +from gi.repository import Gtk +from gi.repository.GLib import timeout_add + +import deluge.component as component +from deluge.common import fsize, fspeed, get_pixmap +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +from .common import build_menu_radio_list +from .dialogs import OtherDialog + +log = logging.getLogger(__name__) + + +class StatusBarItem: + def __init__( + self, + image=None, + stock=None, + icon=None, + text=None, + markup=False, + callback=None, + tooltip=None, + ): + self._widgets = [] + self._ebox = Gtk.EventBox() + self._hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=3) + self._image = Gtk.Image() + self._label = Gtk.Label() + if image or icon or stock: + self._hbox.add(self._image) + self._hbox.add(self._label) + self._ebox.add(self._hbox) + + # Add image from file or stock + if image: + self.set_image_from_file(image) + if stock: + self.set_image_from_stock(stock) + if icon: + self.set_image_from_icon(icon) + + # Add text + if markup: + self.set_markup(text) + else: + self.set_text(text) + + if callback is not None: + self.set_callback(callback) + + if tooltip: + self.set_tooltip(tooltip) + + self.show_all() + + def set_callback(self, callback): + self._ebox.connect('button-press-event', callback) + + def show_all(self): + self._ebox.show() + self._hbox.show() + self._image.show() + + def set_image_from_file(self, image): + self._image.set_from_file(image) + + def set_image_from_stock(self, stock): + self._image.set_from_stock(stock, Gtk.IconSize.MENU) + + def set_image_from_icon(self, icon): + self._image.set_from_icon_name(icon, Gtk.IconSize.MENU) + + def set_text(self, text): + if not text: + self._label.hide() + elif self._label.get_text() != text: + self._label.set_text(text) + self._label.show() + + def set_markup(self, text): + if not text: + self._label.hide() + elif self._label.get_text() != text: + self._label.set_markup(text) + self._label.show() + + def set_tooltip(self, tip): + if self._ebox.get_tooltip_text() != tip: + self._ebox.set_tooltip_text(tip) + + def get_widgets(self): + return self._widgets + + def get_eventbox(self): + return self._ebox + + def get_text(self): + return self._label.get_text() + + +class StatusBar(component.Component): + def __init__(self): + component.Component.__init__(self, 'StatusBar', interval=3) + main_builder = component.get('MainWindow').get_builder() + self.statusbar = main_builder.get_object('statusbar') + self.config = ConfigManager('gtk3ui.conf') + + # Status variables that are updated via callback + self.max_connections_global = -1 + self.num_connections = 0 + self.max_download_speed = -1.0 + self.download_rate = '' + self.max_upload_speed = -1.0 + self.upload_rate = '' + self.dht_nodes = 0 + self.dht_status = False + self.health = False + self.download_protocol_rate = 0.0 + self.upload_protocol_rate = 0.0 + + self.config_value_changed_dict = { + 'max_connections_global': self._on_max_connections_global, + 'max_download_speed': self._on_max_download_speed, + 'max_upload_speed': self._on_max_upload_speed, + 'dht': self._on_dht, + } + self.current_warnings = [] + # Add hbox to the statusbar after removing the initial label widget + self.hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=10) + self.hbox.set_margin_top(2) + self.hbox.set_margin_bottom(3) + frame = self.statusbar.get_children()[0] + frame.remove(frame.get_children()[0]) + frame.add(self.hbox) + self.statusbar.show_all() + # Create the not connected item + self.not_connected_item = StatusBarItem( + icon='network-offline-symbolic', + text=_('Not Connected'), + callback=self._on_notconnected_item_clicked, + ) + # Show the not connected status bar + self.show_not_connected() + + # Hide if necessary + self.visible(self.config['show_statusbar']) + + client.register_event_handler( + 'ConfigValueChangedEvent', self.on_configvaluechanged_event + ) + + def start(self): + # Add in images and labels + self.remove_item(self.not_connected_item) + + self.connections_item = self.add_item( + icon='network-transmit-receive-symbolic', + callback=self._on_connection_item_clicked, + tooltip=_('Connections (Limit)'), + pack_start=True, + ) + + self.download_item = self.add_item( + image=get_pixmap('downloading16.png'), + callback=self._on_download_item_clicked, + tooltip=_('Download Speed (Limit)'), + pack_start=True, + ) + + self.upload_item = self.add_item( + image=get_pixmap('seeding16.png'), + callback=self._on_upload_item_clicked, + tooltip=_('Upload Speed (Limit)'), + pack_start=True, + ) + + self.traffic_item = self.add_item( + image=get_pixmap('traffic16.png'), + callback=self._on_traffic_item_clicked, + tooltip=_('Protocol Traffic (Down:Up)'), + pack_start=True, + ) + + self.dht_item = StatusBarItem( + image=get_pixmap('dht16.png'), tooltip=_('DHT Nodes') + ) + + self.diskspace_item = self.add_item( + icon='drive-harddisk-symbolic', + callback=self._on_diskspace_item_clicked, + tooltip=_('Free Disk Space'), + pack_start=True, + ) + + self.external_ip_item = self.add_item( + tooltip=_('External IP Address'), + text=_('<b>IP</b> <small>%s</small>') % _('n/a'), + markup=True, + pack_start=True, + ) + + self.health_item = self.add_item( + icon='network-error-symbolic', + text=_('<b><small>Port Issue</small></b>'), + markup=True, + tooltip=_('No incoming connections, check port forwarding'), + callback=self._on_health_icon_clicked, + ) + + self.health = False + + def update_config_values(configs): + self._on_max_connections_global(configs['max_connections_global']) + self._on_max_download_speed(configs['max_download_speed']) + self._on_max_upload_speed(configs['max_upload_speed']) + self._on_dht(configs['dht']) + + # Get some config values + client.core.get_config_values( + ['max_connections_global', 'max_download_speed', 'max_upload_speed', 'dht'] + ).addCallback(update_config_values) + + def stop(self): + # When stopped, we just show the not connected thingy + try: + self.remove_item(self.connections_item) + self.remove_item(self.dht_item) + self.remove_item(self.download_item) + self.remove_item(self.upload_item) + self.remove_item(self.not_connected_item) + self.remove_item(self.health_item) + self.remove_item(self.traffic_item) + self.remove_item(self.diskspace_item) + self.remove_item(self.external_ip_item) + except Exception as ex: + log.debug('Unable to remove StatusBar item: %s', ex) + self.show_not_connected() + + def visible(self, visible): + if visible: + self.statusbar.show() + else: + self.statusbar.hide() + + self.config['show_statusbar'] = visible + + def show_not_connected(self): + self.hbox.pack_start(self.not_connected_item.get_eventbox(), False, False, 0) + + def add_item( + self, + image=None, + stock=None, + icon=None, + text=None, + markup=False, + callback=None, + tooltip=None, + pack_start=False, + ): + """Adds an item to the status bar""" + # The return tuple.. we return whatever widgets we add + item = StatusBarItem(image, stock, icon, text, markup, callback, tooltip) + if pack_start: + self.hbox.pack_start(item.get_eventbox(), False, False, 0) + else: + self.hbox.pack_end(item.get_eventbox(), False, False, 0) + return item + + def remove_item(self, item): + """Removes an item from the statusbar""" + if item.get_eventbox() in self.hbox.get_children(): + try: + self.hbox.remove(item.get_eventbox()) + except Exception as ex: + log.debug('Unable to remove widget: %s', ex) + + def add_timeout_item( + self, seconds=3, image=None, stock=None, icon=None, text=None, callback=None + ): + """Adds an item to the StatusBar for seconds""" + item = self.add_item(image, stock, icon, text, callback) + # Start a timer to remove this item in seconds + timeout_add(seconds * 1000, self.remove_item, item) + + def display_warning(self, text, callback=None): + """Displays a warning to the user in the status bar""" + if text not in self.current_warnings: + item = self.add_item( + icon='dialog-warning-symbolic', text=text, callback=callback + ) + self.current_warnings.append(text) + timeout_add(3000, self.remove_warning, item) + + def remove_warning(self, item): + self.current_warnings.remove(item.get_text()) + self.remove_item(item) + + def clear_statusbar(self): + def remove(child): + self.hbox.remove(child) + + self.hbox.foreach(remove) + + def send_status_request(self): + # Sends an async request for data from the core + keys = [ + 'peer.num_peers_connected', + 'upload_rate', + 'download_rate', + 'payload_upload_rate', + 'payload_download_rate', + 'net.sent_bytes', + 'net.recv_bytes', + 'net.sent_payload_bytes', + 'net.recv_payload_bytes', + ] + + if self.dht_status: + keys.append('dht.dht_nodes') + + if not self.health: + keys.append('net.has_incoming_connections') + + client.core.get_session_status(keys).addCallback(self._on_get_session_status) + client.core.get_free_space().addCallback(self._on_get_free_space) + client.core.get_external_ip().addCallback(self._on_get_external_ip) + + def on_configvaluechanged_event(self, key, value): + """ + This is called when we receive a ConfigValueChangedEvent from + the core. + """ + if key in self.config_value_changed_dict: + self.config_value_changed_dict[key](value) + + def _on_max_connections_global(self, max_connections): + self.max_connections_global = max_connections + self.update_connections_label() + + def _on_dht(self, value): + self.dht_status = value + if value: + self.hbox.pack_start(self.dht_item.get_eventbox(), False, False, 0) + self.send_status_request() + else: + self.remove_item(self.dht_item) + + def _on_get_session_status(self, status): + self.download_rate = fspeed( + status['payload_download_rate'], precision=0, shortform=True + ) + self.upload_rate = fspeed( + status['payload_upload_rate'], precision=0, shortform=True + ) + self.download_protocol_rate = ( + status['download_rate'] - status['payload_download_rate'] + ) // 1024 + self.upload_protocol_rate = ( + status['upload_rate'] - status['payload_upload_rate'] + ) // 1024 + self.num_connections = status['peer.num_peers_connected'] + self.update_download_label() + self.update_upload_label() + self.update_traffic_label() + self.update_connections_label() + + if 'dht.dht_nodes' in status: + self.dht_nodes = status['dht.dht_nodes'] + self.update_dht_label() + + if 'net.has_incoming_connections' in status: + self.health = status['net.has_incoming_connections'] + if self.health: + self.remove_item(self.health_item) + + def _on_get_free_space(self, space): + if space >= 0: + self.diskspace_item.set_markup( + '<small>%s</small>' % fsize(space, shortform=True) + ) + else: + self.diskspace_item.set_markup( + '<span foreground="red">' + _('Error') + '</span>' + ) + + def _on_max_download_speed(self, max_download_speed): + self.max_download_speed = max_download_speed + self.update_download_label() + + def _on_max_upload_speed(self, max_upload_speed): + self.max_upload_speed = max_upload_speed + self.update_upload_label() + + def _on_get_external_ip(self, external_ip): + ip = external_ip if external_ip else _('n/a') + self.external_ip_item.set_markup(_('<b>IP</b> <small>%s</small>') % ip) + + def update_connections_label(self): + # Set the max connections label + if self.max_connections_global < 0: + label_string = '%s' % self.num_connections + else: + label_string = '{} <small>({})</small>'.format( + self.num_connections, + self.max_connections_global, + ) + + self.connections_item.set_markup(label_string) + + if self.num_connections: + self.connections_item.set_image_from_icon( + 'network-transmit-receive-symbolic' + ) + else: + self.connections_item.set_image_from_icon('network-idle-symbolic') + + def update_dht_label(self): + # Set the max connections label + self.dht_item.set_markup('<small>%s</small>' % (self.dht_nodes)) + + def update_download_label(self): + # Set the download speed label + if self.max_download_speed <= 0: + label_string = self.download_rate + else: + label_string = '%s <small>(%i %s)</small>' % ( + self.download_rate, + self.max_download_speed, + _('K/s'), + ) + + self.download_item.set_markup(label_string) + + def update_upload_label(self): + # Set the upload speed label + if self.max_upload_speed <= 0: + label_string = self.upload_rate + else: + label_string = '%s <small>(%i %s)</small>' % ( + self.upload_rate, + self.max_upload_speed, + _('K/s'), + ) + + self.upload_item.set_markup(label_string) + + def update_traffic_label(self): + label_string = '<small>%i:%i %s</small>' % ( + self.download_protocol_rate, + self.upload_protocol_rate, + _('K/s'), + ) + self.traffic_item.set_markup(label_string) + + def update(self): + 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'), + _('K/s'), + 'downloading.svg', + self.max_download_speed, + ), + 'max_upload_speed': ( + _('Upload Speed Limit'), + _('Set the maximum upload speed'), + _('K/s'), + 'seeding.svg', + self.max_upload_speed, + ), + 'max_connections_global': ( + _('Incoming Connections'), + _('Set the maximum incoming connections'), + '', + 'network-transmit-receive-symbolic', + 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.ResponseType.OK: + set_value(dialog.get_value()) + + dialog = 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): + self.menu = build_menu_radio_list( + self.config['tray_download_speed_list'], + self._on_set_download_speed, + self.max_download_speed, + _('K/s'), + show_notset=True, + show_other=True, + ) + self.menu.show_all() + self.menu.popup(None, None, None, None, event.button, event.time) + + def _on_set_download_speed(self, widget): + log.debug('_on_set_download_speed') + self.set_limit_value(widget, 'max_download_speed') + + def _on_upload_item_clicked(self, widget, event): + self.menu = build_menu_radio_list( + self.config['tray_upload_speed_list'], + self._on_set_upload_speed, + self.max_upload_speed, + _('K/s'), + show_notset=True, + show_other=True, + ) + self.menu.show_all() + self.menu.popup(None, None, None, None, event.button, event.time) + + def _on_set_upload_speed(self, widget): + log.debug('_on_set_upload_speed') + self.set_limit_value(widget, 'max_upload_speed') + + def _on_connection_item_clicked(self, widget, event): + self.menu = build_menu_radio_list( + self.config['connection_limit_list'], + self._on_set_connection_limit, + self.max_connections_global, + show_notset=True, + show_other=True, + ) + self.menu.show_all() + self.menu.popup(None, None, None, None, event.button, event.time) + + def _on_set_connection_limit(self, widget): + log.debug('_on_set_connection_limit') + self.set_limit_value(widget, 'max_connections_global') + + def _on_health_icon_clicked(self, widget, event): + component.get('Preferences').show('network') + + def _on_notconnected_item_clicked(self, widget, event): + component.get('ConnectionManager').show() + + def _on_traffic_item_clicked(self, widget, event): + component.get('Preferences').show('network') + + def _on_diskspace_item_clicked(self, widget, event): + component.get('Preferences').show('downloads') diff --git a/deluge/ui/gtk3/systemtray.py b/deluge/ui/gtk3/systemtray.py new file mode 100644 index 0000000..5318cf2 --- /dev/null +++ b/deluge/ui/gtk3/systemtray.py @@ -0,0 +1,445 @@ +# +# Copyright (C) 2007, 2008 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. +# + +import logging +import os + +from gi import require_version +from gi.repository.Gtk import Builder, RadioMenuItem, StatusIcon + +import deluge.component as component +from deluge.common import ( + fspeed, + get_pixmap, + osx_check, + resource_filename, + windows_check, +) +from deluge.configmanager import ConfigManager +from deluge.ui.client import client + +from .common import build_menu_radio_list, get_logo +from .dialogs import OtherDialog + +try: + try: + require_version('AyatanaAppIndicator3', '0.1') + from gi.repository import AyatanaAppIndicator3 as AppIndicator3 + except (ValueError, ImportError): + require_version('AppIndicator3', '0.1') + from gi.repository import AppIndicator3 +except (ValueError, ImportError): + AppIndicator3 = None + +log = logging.getLogger(__name__) + + +class SystemTray(component.Component): + def __init__(self): + component.Component.__init__(self, 'SystemTray', interval=4) + self.mainwindow = component.get('MainWindow') + self.config = ConfigManager('gtk3ui.conf') + # List of widgets that need to be hidden when not connected to a host + self.hide_widget_list = [ + 'menuitem_add_torrent', + 'menuitem_pause_session', + 'menuitem_resume_session', + 'menuitem_download_limit', + 'menuitem_upload_limit', + 'menuitem_quitdaemon', + 'separatormenuitem1', + 'separatormenuitem2', + 'separatormenuitem3', + 'separatormenuitem4', + ] + self.config.register_set_function( + 'enable_system_tray', self.on_enable_system_tray_set + ) + # bit of a hack to prevent function from doing something on startup + self.__enabled_set_once = False + self.config.register_set_function( + 'enable_appindicator', self.on_enable_appindicator_set + ) + + self.max_download_speed = -1.0 + self.download_rate = 0.0 + self.max_upload_speed = -1.0 + self.upload_rate = 0.0 + + self.config_value_changed_dict = { + 'max_download_speed': self._on_max_download_speed, + 'max_upload_speed': self._on_max_upload_speed, + } + + def enable(self): + """Enables the system tray icon.""" + self.builder = Builder() + self.builder.add_from_file( + resource_filename(__package__, os.path.join('glade', 'tray_menu.ui')) + ) + + self.builder.connect_signals(self) + + self.tray_menu = self.builder.get_object('tray_menu') + + if AppIndicator3 and self.config['enable_appindicator']: + log.debug('Enabling the Application Indicator...') + self.indicator = AppIndicator3.Indicator.new( + 'deluge', + 'deluge-panel', + AppIndicator3.IndicatorCategory.APPLICATION_STATUS, + ) + self.indicator.set_property('title', _('Deluge')) + + # Pass the menu to the Application Indicator + self.indicator.set_menu(self.tray_menu) + + # Make sure the status of the Show Window MenuItem is correct + self._sig_win_hide = self.mainwindow.window.connect( + 'hide', self._on_window_hide + ) + self._sig_win_show = self.mainwindow.window.connect( + 'show', self._on_window_show + ) + if self.mainwindow.visible(): + self.builder.get_object('menuitem_show_deluge').set_active(True) + else: + self.builder.get_object('menuitem_show_deluge').set_active(False) + + # Show the Application Indicator + self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) + + else: + log.debug('Enabling the system tray icon..') + if windows_check() or osx_check(): + self.tray = StatusIcon.new_from_pixbuf(get_logo(32)) + else: + self.tray = StatusIcon.new_from_icon_name('deluge-panel') + + self.tray.connect('activate', self.on_tray_clicked) + self.tray.connect('popup-menu', self.on_tray_popup) + + self.builder.get_object('download-limit-image').set_from_file( + get_pixmap('downloading16.png') + ) + self.builder.get_object('upload-limit-image').set_from_file( + get_pixmap('seeding16.png') + ) + + client.register_event_handler( + 'ConfigValueChangedEvent', self.config_value_changed + ) + if client.connected(): + # We're connected so we need to get some values from the core + self.__start() + else: + # Hide menu widgets because we're not connected to a host. + for widget in self.hide_widget_list: + self.builder.get_object(widget).hide() + + def __start(self): + if self.config['enable_system_tray']: + if self.config['standalone']: + try: + self.hide_widget_list.remove('menuitem_quitdaemon') + self.hide_widget_list.remove('separatormenuitem4') + except ValueError: + pass + self.builder.get_object('menuitem_quitdaemon').hide() + self.builder.get_object('separatormenuitem4').hide() + + # Show widgets in the hide list because we've connected to a host + for widget in self.hide_widget_list: + self.builder.get_object(widget).show() + + # Build the bandwidth speed limit menus + self.build_tray_bwsetsubmenu() + + # Get some config values + def update_config_values(configs): + self._on_max_download_speed(configs['max_download_speed']) + self._on_max_upload_speed(configs['max_upload_speed']) + + client.core.get_config_values( + ['max_download_speed', 'max_upload_speed'] + ).addCallback(update_config_values) + + def start(self): + self.__start() + + def stop(self): + if self.config['enable_system_tray'] and not self.config['enable_appindicator']: + try: + # Hide widgets in hide list because we're not connected to a host + for widget in self.hide_widget_list: + self.builder.get_object(widget).hide() + except Exception as ex: + log.debug('Unable to hide system tray menu widgets: %s', ex) + + self.tray.set_tooltip_text(_('Deluge') + '\n' + _('Not Connected...')) + + def shutdown(self): + if self.config['enable_system_tray']: + if AppIndicator3 and self.config['enable_appindicator']: + self.indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) + else: + self.tray.set_visible(False) + + def send_status_request(self): + client.core.get_session_status( + ['payload_upload_rate', 'payload_download_rate'] + ).addCallback(self._on_get_session_status) + + def config_value_changed(self, key, value): + """This is called when we received a config_value_changed signal from + the core.""" + if key in self.config_value_changed_dict: + self.config_value_changed_dict[key](value) + + def _on_max_download_speed(self, max_download_speed): + if self.max_download_speed != max_download_speed: + self.max_download_speed = max_download_speed + self.build_tray_bwsetsubmenu() + + def _on_max_upload_speed(self, max_upload_speed): + if self.max_upload_speed != max_upload_speed: + self.max_upload_speed = max_upload_speed + self.build_tray_bwsetsubmenu() + + def _on_get_session_status(self, status): + self.download_rate = fspeed(status['payload_download_rate'], shortform=True) + self.upload_rate = fspeed(status['payload_upload_rate'], shortform=True) + + def update(self): + if not self.config['enable_system_tray']: + return + + # Tool tip text not available for appindicator + if AppIndicator3 and self.config['enable_appindicator']: + if self.mainwindow.visible(): + self.builder.get_object('menuitem_show_deluge').set_active(True) + else: + self.builder.get_object('menuitem_show_deluge').set_active(False) + return + + # Set the tool tip text + max_download_speed = self.max_download_speed + max_upload_speed = self.max_upload_speed + + if max_download_speed == -1: + max_download_speed = _('Unlimited') + else: + max_download_speed = '{} {}'.format(max_download_speed, _('K/s')) + if max_upload_speed == -1: + max_upload_speed = _('Unlimited') + else: + max_upload_speed = '{} {}'.format(max_upload_speed, _('K/s')) + + msg = '{}\n{}: {} ({})\n{}: {} ({})'.format( + _('Deluge'), + _('Down'), + self.download_rate, + max_download_speed, + _('Up'), + self.upload_rate, + max_upload_speed, + ) + + # Set the tooltip + self.tray.set_tooltip_text(msg) + + self.send_status_request() + + def build_tray_bwsetsubmenu(self): + # Create the Download speed list sub-menu + submenu_bwdownset = build_menu_radio_list( + self.config['tray_download_speed_list'], + self.on_tray_setbwdown, + self.max_download_speed, + _('K/s'), + show_notset=True, + show_other=True, + ) + + # Create the Upload speed list sub-menu + submenu_bwupset = build_menu_radio_list( + self.config['tray_upload_speed_list'], + self.on_tray_setbwup, + self.max_upload_speed, + _('K/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 + ) + self.builder.get_object('menuitem_upload_limit').set_submenu(submenu_bwupset) + + # Show the sub-menus for all to see + submenu_bwdownset.show_all() + submenu_bwupset.show_all() + + def disable(self, invert_app_ind_conf=False): + """Disables the system tray icon or Appindicator.""" + try: + if invert_app_ind_conf: + app_ind_conf = not self.config['enable_appindicator'] + else: + app_ind_conf = self.config['enable_appindicator'] + if AppIndicator3 and app_ind_conf: + if hasattr(self, '_sig_win_hide'): + self.mainwindow.window.disconnect(self._sig_win_hide) + self.mainwindow.window.disconnect(self._sig_win_show) + log.debug('Disabling the application indicator..') + + self.indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) + del self.indicator + else: + log.debug('Disabling the system tray icon..') + self.tray.set_visible(False) + del self.tray + del self.builder + del self.tray_menu + except Exception as ex: + log.debug('Unable to disable system tray: %s', ex) + + def blink(self, value): + try: + self.tray.set_blinking(value) + except AttributeError: + # If self.tray is not defined then ignore. This happens when the + # tray icon is not being used. + pass + + def on_enable_system_tray_set(self, key, value): + """Called whenever the 'enable_system_tray' config key is modified""" + if value: + self.enable() + else: + self.disable() + + def on_enable_appindicator_set(self, key, value): + """Called whenever the 'enable_appindicator' config key is modified""" + if self.__enabled_set_once: + self.disable(True) + self.enable() + self.__enabled_set_once = True + + def on_tray_clicked(self, icon): + """Called when the tray icon is left clicked.""" + self.blink(False) + + if self.mainwindow.active(): + self.mainwindow.hide() + else: + self.mainwindow.present() + + def on_tray_popup(self, status_icon, button, activate_time): + """Called when the tray icon is right clicked.""" + self.blink(False) + + if self.mainwindow.visible(): + self.builder.get_object('menuitem_show_deluge').set_active(True) + else: + self.builder.get_object('menuitem_show_deluge').set_active(False) + + popup_function = StatusIcon.position_menu + if windows_check() or osx_check(): + popup_function = None + button = 0 + self.tray_menu.popup( + None, None, popup_function, status_icon, button, activate_time + ) + + def on_menuitem_show_deluge_activate(self, menuitem): + log.debug('on_menuitem_show_deluge_activate') + if menuitem.get_active() and not self.mainwindow.visible(): + self.mainwindow.present() + elif not menuitem.get_active() and self.mainwindow.visible(): + self.mainwindow.hide() + + def on_menuitem_add_torrent_activate(self, menuitem): + log.debug('on_menuitem_add_torrent_activate') + component.get('AddTorrentDialog').show() + + def on_menuitem_pause_session_activate(self, menuitem): + log.debug('on_menuitem_pause_session_activate') + client.core.pause_session() + + def on_menuitem_resume_session_activate(self, menuitem): + log.debug('on_menuitem_resume_session_activate') + client.core.resume_session() + + def on_menuitem_quit_activate(self, menuitem): + log.debug('on_menuitem_quit_activate') + self.mainwindow.quit() + + def on_menuitem_quitdaemon_activate(self, menuitem): + log.debug('on_menuitem_quitdaemon_activate') + self.mainwindow.quit(shutdown=True) + + def on_tray_setbwdown(self, widget, data=None): + if isinstance(widget, RadioMenuItem): + # ignore previous radiomenuitem value + if not widget.get_active(): + return + 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, RadioMenuItem): + # ignore previous radiomenuitem value + if not widget.get_active(): + return + 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""" + log.debug('_on_window_hide') + self.builder.get_object('menuitem_show_deluge').set_active(False) + + def _on_window_show(self, widget, data=None): + """_on_window_show - update the menuitem's status""" + log.debug('_on_window_show') + self.builder.get_object('menuitem_show_deluge').set_active(True) + + def setbwlimit(self, widget, header, text, core_key, ui_key, default, image): + """Sets the bandwidth limit based on the user selection.""" + + 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}) + + if widget.get_name() == 'unlimited': + set_value(-1) + elif widget.get_name() == 'other': + dialog = OtherDialog(header, text, _('K/s'), image, default) + dialog.run().addCallback(set_value) + else: + set_value(widget.get_children()[0].get_text().split(' ')[0]) diff --git a/deluge/ui/gtk3/tab_data_funcs.py b/deluge/ui/gtk3/tab_data_funcs.py new file mode 100644 index 0000000..a78994f --- /dev/null +++ b/deluge/ui/gtk3/tab_data_funcs.py @@ -0,0 +1,93 @@ +# +# Copyright (C) 2007, 2008 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. +# + +from deluge.common import fdate, fsize, fspeed, ftime +from deluge.ui.common import TRACKER_STATUS_TRANSLATION + + +def ftotal_sized(first, second): + return f'{fsize(first, shortform=True)} ({fsize(second, shortform=True)})' + + +def fratio(value): + return ('%.3f' % value).rstrip('0').rstrip('.') if value > 0 else '∞' + + +def fpcnt(value, state, message): + state_i18n = _(state) + if state not in ('Error', 'Seeding') and value < 100: + percent = f'{value:.2f}'.rstrip('0').rstrip('.') + return _('{state} {percent}%').format(state=state_i18n, percent=percent) + elif state == 'Error': + return _('{state}: {err_msg}').format(state=state_i18n, err_msg=message) + else: + return state_i18n + + +def fspeed_max(value, max_value=-1): + value = fspeed(value, shortform=True) + return '{} ({} {})'.format(value, max_value, _('K/s')) if max_value > -1 else value + + +def fdate_or_never(value): + """Display value as date, eg 05/05/08 or Never""" + return fdate(value, date_only=True) if value > 0 else _('Never') + + +def fdate_or_dash(value): + """Display value as date, eg 05/05/08 or dash""" + if value > 0.0: + return fdate(value) + else: + return '-' + + +def ftime_or_dash(value): + """Display value as time, eg 2h 30m or dash""" + if value > 0: + return ftime(value) + elif value == 0: + return '-' + else: + return '∞' + + +def fseed_rank_or_dash(seed_rank, seeding_time): + """Display value if seeding otherwise dash""" + + if seeding_time > 0: + if seed_rank >= 1000: + return '%i k' % (seed_rank // 1000) + else: + return str(seed_rank) + else: + return '-' + + +def fpieces_num_size(num_pieces, piece_size): + return f'{num_pieces} ({fsize(piece_size, precision=0)})' + + +def fcount(value): + return '%s' % len(value) + + +def ftranslate(text): + if text in TRACKER_STATUS_TRANSLATION: + text = _(text) + elif text: + for status in TRACKER_STATUS_TRANSLATION: + if status in text: + text = text.replace(status, _(status)) + break + return text + + +def fyes_no(value): + """Return Yes or No to bool value""" + return _('Yes') if value else _('No') diff --git a/deluge/ui/gtk3/toolbar.py b/deluge/ui/gtk3/toolbar.py new file mode 100644 index 0000000..1b6952e --- /dev/null +++ b/deluge/ui/gtk3/toolbar.py @@ -0,0 +1,131 @@ +# +# Copyright (C) 2007-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. +# + +import logging + +from gi.repository.Gtk import SeparatorToolItem, ToolButton + +import deluge.component as component +from deluge.configmanager import ConfigManager + +log = logging.getLogger(__name__) + + +class ToolBar(component.Component): + def __init__(self): + component.Component.__init__(self, 'ToolBar') + log.debug('ToolBar Init..') + mainwindow = component.get('MainWindow') + self.main_builder = mainwindow.get_builder() + self.toolbar = self.main_builder.get_object('toolbar') + self.config = ConfigManager('gtk3ui.conf') + # Connect main window Signals # + mainwindow.connect_signals(self) + self.change_sensitivity = [ + 'toolbutton_add', + 'toolbutton_remove', + 'toolbutton_pause', + 'toolbutton_resume', + 'toolbutton_queue_up', + 'toolbutton_queue_down', + 'toolbutton_filter', + 'find_menuitem', + ] + + # Hide if necessary + self.visible(self.config['show_toolbar']) + + def start(self): + self.main_builder.get_object('toolbutton_connectionmanager').set_visible( + not self.config['standalone'] + ) + + for widget in self.change_sensitivity: + self.main_builder.get_object(widget).set_sensitive(True) + + def stop(self): + for widget in self.change_sensitivity: + self.main_builder.get_object(widget).set_sensitive(False) + + def visible(self, visible): + if visible: + self.toolbar.show() + else: + self.toolbar.hide() + + self.config['show_toolbar'] = visible + + def add_toolbutton( + self, callback, label=None, image=None, stock=None, tooltip=None + ): + """Adds a toolbutton to the toolbar""" + toolbutton = ToolButton() + if stock is not None: + toolbutton.set_stock_id(stock) + if label is not None: + toolbutton.set_label(label) + if image is not None: + toolbutton.set_icon_widget(image) + if tooltip is not None: + toolbutton.set_tooltip_text(tooltip) + + toolbutton.connect('clicked', callback) + self.toolbar.insert(toolbutton, -1) + toolbutton.show_all() + + return toolbutton + + def add_separator(self, position=None): + """Adds a separator toolitem""" + sep = SeparatorToolItem() + if position is not None: + self.toolbar.insert(sep, position) + else: + self.toolbar.insert(sep, -1) + + sep.show() + + return sep + + def remove(self, widget): + """Removes a widget from the toolbar""" + self.toolbar.remove(widget) + + # Callbacks (Uses the menubar's callback) # + + def on_toolbutton_add_clicked(self, data): + log.debug('on_toolbutton_add_clicked') + component.get('MenuBar').on_menuitem_addtorrent_activate(data) + + def on_toolbutton_remove_clicked(self, data): + log.debug('on_toolbutton_remove_clicked') + component.get('MenuBar').on_menuitem_remove_activate(data) + + def on_toolbutton_pause_clicked(self, data): + log.debug('on_toolbutton_pause_clicked') + component.get('MenuBar').on_menuitem_pause_activate(data) + + def on_toolbutton_resume_clicked(self, data): + log.debug('on_toolbutton_resume_clicked') + component.get('MenuBar').on_menuitem_resume_activate(data) + + def on_toolbutton_preferences_clicked(self, data): + log.debug('on_toolbutton_preferences_clicked') + component.get('MenuBar').on_menuitem_preferences_activate(data) + + def on_toolbutton_connectionmanager_clicked(self, data): + log.debug('on_toolbutton_connectionmanager_clicked') + component.get('MenuBar').on_menuitem_connectionmanager_activate(data) + + def on_toolbutton_queue_up_clicked(self, data): + log.debug('on_toolbutton_queue_up_clicked') + component.get('MenuBar').on_menuitem_queue_up_activate(data) + + def on_toolbutton_queue_down_clicked(self, data): + log.debug('on_toolbutton_queue_down_clicked') + component.get('MenuBar').on_menuitem_queue_down_activate(data) diff --git a/deluge/ui/gtk3/torrentdetails.py b/deluge/ui/gtk3/torrentdetails.py new file mode 100644 index 0000000..08c37a1 --- /dev/null +++ b/deluge/ui/gtk3/torrentdetails.py @@ -0,0 +1,487 @@ +# +# Copyright (C) 2007 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. +# + + +"""The torrent details component shows info about the selected torrent.""" +import logging +from collections import namedtuple + +from gi.repository.Gtk import ( + CheckMenuItem, + Menu, + MenuItem, + PositionType, + RadioMenuItem, + SeparatorMenuItem, +) + +import deluge.component as component +from deluge.ui.client import client + +from .common import load_pickled_state_file, save_pickled_state_file + +log = logging.getLogger(__name__) + +TabWidget = namedtuple('TabWidget', ('obj', 'func', 'status_keys')) + + +class Tab: + def __init__(self, name=None, child_widget=None, tab_label=None): + self._name = name + self.is_visible = True + self.position = -1 + self.weight = -1 + + self.main_builder = component.get('MainWindow').get_builder() + self._child_widget = ( + self.main_builder.get_object(child_widget) if child_widget else None + ) + self._tab_label = self.main_builder.get_object(tab_label) if tab_label else None + + self.tab_widgets = {} + self.status_keys = [] + + def get_name(self): + return self._name + + def get_child_widget(self): + parent = self._child_widget.get_parent() + if parent is not None: + parent.remove(self._child_widget) + + return self._child_widget + + def get_tab_label(self): + parent = self._tab_label.get_parent() + log.debug('parent: %s', parent) + if parent is not None: + parent.remove(self._tab_label) + + return self._tab_label + + def widget_status_as_fstr(self, widget, status): + """Use TabWidget status_key and func to format status string. + + Args: + widget (TabWidget): A tuple of widget object, func and status_keys. + status (dict): Torrent status dict. + + Returns: + str: The formatted status string. + """ + try: + if widget.func is None: + txt = status[widget.status_keys[0]] + else: + args = [status[key] for key in widget.status_keys] + txt = widget.func(*args) + except KeyError as ex: + log.warning('Unable to get status value: %s', ex) + txt = '' + return txt + + def add_tab_widget(self, widget_id, format_func, status_keys): + """Create TabWidget item in tab_widgets dictionary. + + Args: + widget_id (str): The widget id used to retrieve widget from mainwindow builder. + format_func (str): A func name related to widget e.g. string label formatter. + status_keys (list): List of status keys to lookup for the widget. + + """ + widget_obj = self.main_builder.get_object(widget_id) + self.status_keys.extend(status_keys) + # Store the widget in a tab_widgets dict with name as key for faster lookup. + self.tab_widgets[widget_id] = TabWidget(widget_obj, format_func, status_keys) + + +class TorrentDetails(component.Component): + def __init__(self): + component.Component.__init__(self, 'TorrentDetails', interval=2) + main_builder = component.get('MainWindow').get_builder() + self.config = component.get('MainWindow').config + + self.notebook = main_builder.get_object('torrent_info') + self.notebook.set_tab_pos( + getattr(PositionType, self.config['tabsbar_tab_pos'].upper()) + ) + + # This is the menu item we'll attach the tabs checklist menu to + self.menu_tabs = main_builder.get_object('menu_tabs') + + self.notebook.connect('switch-page', self._on_switch_page) + + # Tabs holds references to the Tab objects by their name + self.tabs = {} + + # Add the default tabs + from .details_tab import DetailsTab + from .files_tab import FilesTab + from .options_tab import OptionsTab + from .peers_tab import PeersTab + from .status_tab import StatusTab + from .trackers_tab import TrackersTab + + default_tabs = { + 'Status': StatusTab, + 'Details': DetailsTab, + 'Files': FilesTab, + 'Peers': PeersTab, + 'Options': OptionsTab, + 'Trackers': TrackersTab, + } + + # tab_name, visible + default_order = [ + ('Status', True), + ('Details', True), + ('Options', True), + ('Files', True), + ('Peers', True), + ('Trackers', True), + ] + + self.translate_tabs = { + 'All': _('_All'), + 'Status': _('_Status'), + 'Details': _('_Details'), + 'Files': _('Fi_les'), + 'Peers': _('_Peers'), + 'Options': _('_Options'), + 'Trackers': _('_Trackers'), + } + + # Get the state from saved file + state = self.load_state() + + if state: + for item in state: + if not isinstance(item, tuple): + log.debug('Old tabs.state, using default..') + state = None + break + + # The state is a list of tab_names in the order they should appear + if state is None: + # Set the default order + state = default_order + + # We need to rename the tab in the state for backwards compat + self.state = [ + (tab_name.replace('Statistics', 'Status'), visible) + for tab_name, visible in state + ] + + for tab in default_tabs.values(): + self.add_tab(tab(), generate_menu=False) + + # Generate the checklist menu + self.generate_menu() + + self.visible(self.config['show_tabsbar']) + + def tab_insert_position(self, weight): + """Returns the position a tab with a given weight should be inserted in""" + # Determine insert position based on weight + # weights is a list of visible tab names in weight order + + weights = sorted( + (tab.weight, name) for name, tab in self.tabs.items() if tab.is_visible + ) + + log.debug('weights: %s', weights) + log.debug('weight of tab: %s', weight) + + position = -1 + for w, name in weights: + if w >= weight: + position = self.tabs[name].position + log.debug('Found pos %d', position) + break + return position + + def add_tab(self, tab, generate_menu=True, visible=None): + name = tab.get_name() + + # find position of tab in self.state, this is the tab weight + weight = None + for w, item in enumerate(self.state): + if item[0] == name: + weight = w + if visible is None: + visible = item[1] + break + + if weight is None: + if visible is None: + visible = True + weight = len(self.state) + self.state.append((name, visible)) + + tab.weight = weight + + if visible: + tab.is_visible = True + # add the tab at position guided by the weight + insert_pos = self.tab_insert_position(weight) + log.debug('Trying to insert tab at %d', insert_pos) + pos = self.notebook.insert_page( + tab.get_child_widget(), tab.get_tab_label(), insert_pos + ) + log.debug('Tab inserted at %d', pos) + tab.position = pos + if not self.notebook.get_property('visible'): + # If the notebook isn't visible, show it + self.visible(True) + else: + tab.is_visible = False + + self.tabs[name] = tab + if name not in self.translate_tabs: + self.translate_tabs[name] = _(name) + + self.regenerate_positions() + if generate_menu: + self.generate_menu() + + def regenerate_positions(self): + """Sync the positions in the tab, with the position stored in the tab object""" + for tab in self.tabs: + page_num = self.notebook.page_num(self.tabs[tab]._child_widget) + if page_num > -1: + self.tabs[tab].position = page_num + + def remove_tab(self, tab_name): + """Removes a tab by name.""" + self.notebook.remove_page(self.tabs[tab_name].position) + del self.tabs[tab_name] + self.regenerate_positions() + self.generate_menu() + + # If there are no tabs visible, then do not show the notebook + if len(self.tabs) == 0: + self.visible(False) + + def hide_all_tabs(self): + """Hides all tabs""" + log.debug('n_pages: %s', self.notebook.get_n_pages()) + for n in range(self.notebook.get_n_pages() - 1, -1, -1): + self.notebook.remove_page(n) + + for tab in self.tabs: + self.tabs[tab].is_visible = False + log.debug('n_pages: %s', self.notebook.get_n_pages()) + self.generate_menu() + self.visible(False) + + def show_all_tabs(self): + """Shows all tabs""" + for tab in self.tabs: + if not self.tabs[tab].is_visible: + self.show_tab(tab, generate_menu=False) + self.generate_menu() + + def hide_tab(self, tab_name): + """Hides tab by name""" + self.tabs[tab_name].is_visible = False + self.notebook.remove_page(self.tabs[tab_name].position) + self.regenerate_positions() + self.generate_menu() + + show = False + for name, tab in self.tabs.items(): + show = show or tab.is_visible + + self.visible(show) + + def show_tab(self, tab_name, generate_menu=True): + log.debug( + '%s\n%s\n%s', + self.tabs[tab_name].get_child_widget(), + self.tabs[tab_name].get_tab_label(), + self.tabs[tab_name].position, + ) + + position = self.tab_insert_position(self.tabs[tab_name].weight) + + log.debug('position: %s', position) + self.notebook.insert_page( + self.tabs[tab_name].get_child_widget(), + self.tabs[tab_name].get_tab_label(), + position, + ) + self.tabs[tab_name].is_visible = True + self.regenerate_positions() + if generate_menu: + self.generate_menu() + self.visible(True) + + def create_tab_pos_menuitem(self): + """Returns a menu to select which side of the notebook the tabs should be shown""" + tab_pos_menu = Menu() + tab_pos_menuitem = MenuItem.new_with_label(_('Position')) + group = [] + for pos in ('top', 'right', 'bottom', 'left'): + menuitem = RadioMenuItem.new_with_mnemonic(group, _(pos.capitalize())) + group = menuitem.get_group() + menuitem.connect('toggled', self._on_tabs_pos_toggled, pos) + menuitem.set_active(pos == self.notebook.get_tab_pos().value_nick) + tab_pos_menu.append(menuitem) + tab_pos_menuitem.set_submenu(tab_pos_menu) + return tab_pos_menuitem + + def generate_menu(self): + """Generates the checklist menu for all the tabs and attaches it""" + menu = Menu() + + # Create 'All' menuitem and a separator + menuitem = CheckMenuItem.new_with_mnemonic(self.translate_tabs['All']) + menuitem.set_name('All') + + all_tabs = True + for key in self.tabs: + if not self.tabs[key].is_visible: + all_tabs = False + break + menuitem.set_active(all_tabs) + menuitem.connect('toggled', self._on_menuitem_toggled) + + menu.append(menuitem) + + menuitem = SeparatorMenuItem() + menu.append(menuitem) + + # Create a list in order of tabs to create menu + menuitem_list = [] + for tab_name in self.tabs: + menuitem_list.append((self.tabs[tab_name].weight, tab_name)) + menuitem_list.sort() + + for pos, name in menuitem_list: + menuitem = CheckMenuItem.new_with_mnemonic(self.translate_tabs[name]) + menuitem.set_name(name) + menuitem.set_active(self.tabs[name].is_visible) + menuitem.connect('toggled', self._on_menuitem_toggled) + menu.append(menuitem) + + menu.append(SeparatorMenuItem()) + menu.append(self.create_tab_pos_menuitem()) + + self.menu_tabs.set_submenu(menu) + self.menu_tabs.show_all() + + def visible(self, visible): + self.notebook.show() if visible else self.notebook.hide() + self.config['show_tabsbar'] = visible + + def set_tab_visible(self, tab_name, visible): + """Sets the tab to visible""" + log.debug('set_tab_visible name: %s visible: %s', tab_name, visible) + if visible and not self.tabs[tab_name].is_visible: + self.show_tab(tab_name) + elif not visible and self.tabs[tab_name].is_visible: + self.hide_tab(tab_name) + + def start(self): + for tab in self.tabs.values(): + try: + tab.start() + except AttributeError: + pass + + def stop(self): + self.clear() + for tab in self.tabs.values(): + try: + tab.stop() + except AttributeError: + pass + + def shutdown(self): + # Save the state of the tabs + for tab in self.tabs: + try: + self.tabs[tab].save_state() + except AttributeError: + pass + + # Save tabs state + self.save_state() + + def update(self, page_num=None): + if len(component.get('TorrentView').get_selected_torrents()) == 0: + # No torrents selected, so just clear + self.clear() + + if self.notebook.get_property('visible'): + if page_num is None: + page_num = self.notebook.get_current_page() + try: + # Get the tab name + name = None + for tab in self.tabs: + if ( + self.tabs[tab].position == page_num + and self.tabs[tab].is_visible + ): + name = tab + except IndexError: + return + # Update the tab that is in view + if name: + self.tabs[name].update() + + def clear(self): + # Get the tab name + try: + page_num = self.notebook.get_current_page() + name = None + for tab in self.tabs: + if self.tabs[tab].position == page_num and self.tabs[tab].is_visible: + name = tab + if name: + self.tabs[name].clear() + except Exception as ex: + log.debug('Unable to clear torrentdetails: %s', ex) + + def _on_switch_page(self, notebook, page, page_num): + self.update(page_num) + client.force_call(False) + + def _on_menuitem_toggled(self, widget): + # Get the tab name + name = widget.get_name() + if name == 'All': + if widget.get_active(): + self.show_all_tabs() + else: + self.hide_all_tabs() + return + + self.set_tab_visible(name, widget.get_active()) + + def _on_tabs_pos_toggled(self, widget, position): + self.config['tabsbar_tab_pos'] = position + self.notebook.set_tab_pos(getattr(PositionType, position.upper())) + + def save_state(self): + """We save the state, which is basically the tab_index list""" + # Update the visiblity status of all tabs + # Leave tabs we dont know anything about it the state as they + # might come from a plugin + for i, (name, visible) in enumerate(self.state): + log.debug('Testing name: %s', name) + if name in self.tabs: + self.state[i] = (name, self.tabs[name].is_visible) + log.debug('Set to %s', self.state[i]) + state = self.state + + save_pickled_state_file('tabs.state', state) + + def load_state(self): + return load_pickled_state_file('tabs.state') diff --git a/deluge/ui/gtk3/torrentview.py b/deluge/ui/gtk3/torrentview.py new file mode 100644 index 0000000..16de16e --- /dev/null +++ b/deluge/ui/gtk3/torrentview.py @@ -0,0 +1,938 @@ +# +# Copyright (C) 2007, 2008 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. +# + +"""The torrent view component that lists all torrents in the session.""" +import logging +from locale import strcoll + +from gi.repository.Gdk import ModifierType, keyval_name +from gi.repository.GLib import idle_add +from gi.repository.GObject import TYPE_UINT64 +from gi.repository.Gtk import EntryIconPosition +from twisted.internet import reactor + +import deluge.component as component +from deluge.common import decode_bytes +from deluge.ui.client import client + +from . import torrentview_data_funcs as funcs +from .common import cmp +from .listview import ListView +from .removetorrentdialog import RemoveTorrentDialog + +log = logging.getLogger(__name__) + +try: + CTRL_ALT_MASK = ModifierType.CONTROL_MASK | ModifierType.MOD1_MASK +except TypeError: + # Sphinx AutoDoc has a mock issue with Gdk masks. + pass + + +def str_nocase_sort(model, iter1, iter2, data): + """Sort string column data using ISO 14651 in lowercase. + + Uses locale.strcoll which (allegedly) uses ISO 14651. Compares first + value with second and returns -1, 0, 1 for where it should be placed. + + """ + v1 = model[iter1][data] + v2 = model[iter2][data] + # Catch any values of None from model. + v1 = v1.lower() if v1 else '' + v2 = v2.lower() if v2 else '' + return strcoll(v1, v2) + + +def queue_peer_seed_sort_function(v1, v2): + if v1 == v2: + return 0 + if v2 < 0: + return -1 + if v1 < 0: + return 1 + if v1 > v2: + return 1 + if v2 > v1: + return -1 + + +def queue_column_sort(model, iter1, iter2, data): + v1 = model[iter1][data] + v2 = model[iter2][data] + return queue_peer_seed_sort_function(v1, v2) + + +def eta_column_sort(model, iter1, iter2, data): + v1 = model[iter1][data] + v2 = model[iter2][data] + if v1 == v2: + return 0 + if v1 == 0: + return -1 + if v2 == 0: + return 1 + if v1 > v2: + return -1 + if v2 > v1: + return 1 + + +def seed_peer_column_sort(model, iter1, iter2, data): + v1 = model[iter1][data] # num seeds/peers + v3 = model[iter2][data] # num seeds/peers + if v1 == v3: + v2 = model[iter1][data + 1] # total seeds/peers + v4 = model[iter2][data + 1] # total seeds/peers + return queue_peer_seed_sort_function(v2, v4) + return queue_peer_seed_sort_function(v1, v3) + + +def progress_sort(model, iter1, iter2, sort_column_id): + progress1 = model[iter1][sort_column_id] + progress2 = model[iter2][sort_column_id] + # Progress value is equal, so sort on state + if progress1 == progress2: + state1 = model[iter1][sort_column_id + 1] + state2 = model[iter2][sort_column_id + 1] + return cmp(state1, state2) + return cmp(progress1, progress2) + + +class SearchBox: + def __init__(self, torrentview): + self.torrentview = torrentview + mainwindow = component.get('MainWindow') + main_builder = mainwindow.get_builder() + + self.visible = False + self.search_pending = self.prefiltered = None + + self.search_box = main_builder.get_object('search_box') + self.search_torrents_entry = main_builder.get_object('search_torrents_entry') + self.close_search_button = main_builder.get_object('close_search_button') + self.match_search_button = main_builder.get_object('search_torrents_match') + mainwindow.connect_signals(self) + + def show(self): + self.visible = True + self.search_box.show_all() + self.search_torrents_entry.grab_focus() + + def hide(self): + self.visible = False + self.clear_search() + self.search_box.hide() + self.search_pending = self.prefiltered = None + + def clear_search(self): + if self.search_pending and self.search_pending.active(): + self.search_pending.cancel() + + if self.prefiltered: + filter_column = self.torrentview.columns['filter'].column_indices[0] + torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0] + for row in self.torrentview.liststore: + torrent_id = row[torrent_id_column] + + if torrent_id in self.prefiltered: + # Reset to previous filter state + self.prefiltered.pop(self.prefiltered.index(torrent_id)) + row[filter_column] = not row[filter_column] + + self.prefiltered = None + + self.search_torrents_entry.set_text('') + if self.torrentview.filter and 'name' in self.torrentview.filter: + self.torrentview.filter.pop('name', None) + self.search_pending = reactor.callLater(0.5, self.torrentview.update) + + def set_search_filter(self): + if self.search_pending and self.search_pending.active(): + self.search_pending.cancel() + + if self.torrentview.filter and 'name' in self.torrentview.filter: + self.torrentview.filter.pop('name', None) + + elif self.torrentview.filter is None: + self.torrentview.filter = {} + + search_string = self.search_torrents_entry.get_text() + if not search_string: + self.clear_search() + else: + if self.match_search_button.get_active(): + search_string += '::match' + self.torrentview.filter['name'] = search_string + self.prefilter_torrentview() + + def prefilter_torrentview(self): + filter_column = self.torrentview.columns['filter'].column_indices[0] + torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0] + torrent_name_column = self.torrentview.columns[_('Name')].column_indices[1] + + match_case = self.match_search_button.get_active() + if match_case: + search_string = self.search_torrents_entry.get_text() + else: + search_string = self.search_torrents_entry.get_text().lower() + + if self.prefiltered is None: + self.prefiltered = [] + + for row in self.torrentview.liststore: + torrent_id = row[torrent_id_column] + + if torrent_id in self.prefiltered: + # Reset to previous filter state + self.prefiltered.pop(self.prefiltered.index(torrent_id)) + row[filter_column] = not row[filter_column] + + if not row[filter_column]: + # Row is not visible(filtered out, but not by our filter), skip it + continue + + if match_case: + torrent_name = row[torrent_name_column] + else: + torrent_name = row[torrent_name_column].lower() + + if search_string in torrent_name and not row[filter_column]: + row[filter_column] = True + self.prefiltered.append(torrent_id) + elif search_string not in torrent_name and row[filter_column]: + row[filter_column] = False + self.prefiltered.append(torrent_id) + + def on_close_search_button_clicked(self, widget): + self.hide() + + def on_search_filter_toggle(self, widget): + if self.visible: + self.hide() + else: + self.show() + + def on_search_torrents_match_toggled(self, widget): + if self.search_torrents_entry.get_text(): + self.set_search_filter() + self.search_pending = reactor.callLater(0.7, self.torrentview.update) + + def on_search_torrents_entry_icon_press(self, entry, icon, event): + if icon != EntryIconPosition.SECONDARY: + return + self.clear_search() + + def on_search_torrents_entry_changed(self, widget): + self.set_search_filter() + self.search_pending = reactor.callLater(0.7, self.torrentview.update) + + +class TorrentView(ListView, component.Component): + """TorrentView handles the listing of torrents.""" + + def __init__(self): + component.Component.__init__( + self, 'TorrentView', interval=2, depend=['SessionProxy'] + ) + main_builder = component.get('MainWindow').get_builder() + # Call the ListView constructor + ListView.__init__( + self, main_builder.get_object('torrent_view'), 'torrentview.state' + ) + log.debug('TorrentView Init..') + + # If we have gotten the state yet + self.got_state = False + + # This is where status updates are put + self.status = {} + + # We keep a copy of the previous status to compare for changes + self.prev_status = {} + + # Register the columns menu with the listview so it gets updated accordingly. + self.register_checklist_menu(main_builder.get_object('menu_columns')) + + # Add the columns to the listview + self.add_text_column('torrent_id', hidden=True, unique=True) + self.add_bool_column('dirty', hidden=True) + self.add_func_column( + '#', + funcs.cell_data_queue, + [int], + status_field=['queue'], + sort_func=queue_column_sort, + ) + self.add_texticon_column( + _('Name'), + status_field=['state', 'name'], + function=funcs.cell_data_statusicon, + sort_func=str_nocase_sort, + default_sort=True, + ) + self.add_func_column( + _('Size'), + funcs.cell_data_size, + [TYPE_UINT64], + status_field=['total_wanted'], + ) + self.add_func_column( + _('Downloaded'), + funcs.cell_data_size, + [TYPE_UINT64], + status_field=['all_time_download'], + default=False, + ) + self.add_func_column( + _('Uploaded'), + funcs.cell_data_size, + [TYPE_UINT64], + status_field=['total_uploaded'], + default=False, + ) + self.add_func_column( + _('Remaining'), + funcs.cell_data_size, + [TYPE_UINT64], + status_field=['total_remaining'], + default=False, + ) + self.add_progress_column( + _('Progress'), + status_field=['progress', 'state'], + col_types=[float, str], + function=funcs.cell_data_progress, + sort_func=progress_sort, + ) + self.add_func_column( + _('Seeds'), + funcs.cell_data_peer, + [int, int], + status_field=['num_seeds', 'total_seeds'], + sort_func=seed_peer_column_sort, + default=False, + ) + self.add_func_column( + _('Peers'), + funcs.cell_data_peer, + [int, int], + status_field=['num_peers', 'total_peers'], + sort_func=seed_peer_column_sort, + default=False, + ) + self.add_func_column( + _('Seeds:Peers'), + funcs.cell_data_ratio_seeds_peers, + [float], + status_field=['seeds_peers_ratio'], + default=False, + ) + self.add_func_column( + _('Down Speed'), + funcs.cell_data_speed_down, + [int], + status_field=['download_payload_rate'], + ) + self.add_func_column( + _('Up Speed'), + funcs.cell_data_speed_up, + [int], + status_field=['upload_payload_rate'], + ) + self.add_func_column( + _('Down Limit'), + funcs.cell_data_speed_limit_down, + [float], + status_field=['max_download_speed'], + default=False, + ) + self.add_func_column( + _('Up Limit'), + funcs.cell_data_speed_limit_up, + [float], + status_field=['max_upload_speed'], + default=False, + ) + self.add_func_column( + _('ETA'), + funcs.cell_data_time, + [int], + status_field=['eta'], + sort_func=eta_column_sort, + ) + self.add_func_column( + _('Ratio'), + funcs.cell_data_ratio_ratio, + [float], + status_field=['ratio'], + default=False, + ) + self.add_func_column( + _('Avail'), + funcs.cell_data_ratio_avail, + [float], + status_field=['distributed_copies'], + default=False, + ) + self.add_func_column( + _('Added'), + funcs.cell_data_date_added, + [int], + status_field=['time_added'], + default=False, + ) + self.add_func_column( + _('Completed'), + funcs.cell_data_date_completed, + [int], + status_field=['completed_time'], + default=False, + ) + self.add_func_column( + _('Complete Seen'), + funcs.cell_data_date_or_never, + [int], + status_field=['last_seen_complete'], + default=False, + ) + self.add_func_column( + _('Last Transfer'), + funcs.cell_data_time, + [int], + status_field=['time_since_transfer'], + default=False, + ) + self.add_texticon_column( + _('Tracker'), + function=funcs.cell_data_trackericon, + status_field=['tracker_host', 'tracker_host'], + default=False, + ) + self.add_text_column( + _('Download Folder'), status_field=['download_location'], default=False + ) + self.add_text_column(_('Owner'), status_field=['owner'], default=False) + self.add_bool_column( + _('Shared'), + status_field=['shared'], + default=False, + tooltip=_('Torrent is shared between other Deluge users or not.'), + ) + self.restore_columns_order_from_state() + + # Set filter to None for now + self.filter = None + + # Connect Signals # + # Connect to the 'button-press-event' to know when to bring up the + # torrent menu popup. + self.treeview.connect('button-press-event', self.on_button_press_event) + # Connect to the 'key-press-event' to know when the bring up the + # torrent menu popup via keypress. + self.treeview.connect('key-release-event', self.on_key_press_event) + # Connect to the 'changed' event of TreeViewSelection to get selection + # changes. + self.treeview.get_selection().connect('changed', self.on_selection_changed) + + self.treeview.connect('drag-drop', self.on_drag_drop) + self.treeview.connect('drag_data_received', self.on_drag_data_received) + self.treeview.connect('key-press-event', self.on_key_press_event) + self.treeview.connect('columns-changed', self.on_columns_changed_event) + + self.search_box = SearchBox(self) + self.permanent_status_keys = ['owner'] + self.columns_to_update = [] + + def start(self): + """Start the torrentview""" + # We need to get the core session state to know which torrents are in + # the session so we can add them to our list. + # Only get the status fields required for the visible columns + status_fields = [] + for listview_column in self.columns.values(): + if listview_column.column.get_visible(): + if not listview_column.status_field: + continue + status_fields.extend(listview_column.status_field) + component.get('SessionProxy').get_torrents_status( + {}, status_fields + ).addCallback(self._on_session_state) + + client.register_event_handler( + 'TorrentStateChangedEvent', self.on_torrentstatechanged_event + ) + client.register_event_handler('TorrentAddedEvent', self.on_torrentadded_event) + client.register_event_handler( + 'TorrentRemovedEvent', self.on_torrentremoved_event + ) + client.register_event_handler('SessionPausedEvent', self.on_sessionpaused_event) + client.register_event_handler( + 'SessionResumedEvent', self.on_sessionresumed_event + ) + client.register_event_handler( + 'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event + ) + + def _on_session_state(self, state): + self.add_rows(state) + self.got_state = True + # Update the view right away with our status + self.status = state + self.set_columns_to_update() + self.update_view(load_new_list=True) + self.select_first_row() + + def stop(self): + """Stops the torrentview""" + client.deregister_event_handler( + 'TorrentStateChangedEvent', self.on_torrentstatechanged_event + ) + client.deregister_event_handler('TorrentAddedEvent', self.on_torrentadded_event) + client.deregister_event_handler( + 'TorrentRemovedEvent', self.on_torrentremoved_event + ) + client.deregister_event_handler( + 'SessionPausedEvent', self.on_sessionpaused_event + ) + client.deregister_event_handler( + 'SessionResumedEvent', self.on_sessionresumed_event + ) + client.deregister_event_handler( + 'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event + ) + + if self.treeview.get_selection(): + self.treeview.get_selection().unselect_all() + + # Save column state before clearing liststore + # so column sort details are correctly saved. + self.save_state() + self.liststore.clear() + self.prev_status = {} + self.filter = None + self.search_box.hide() + + def shutdown(self): + """Called when GtkUi is exiting""" + pass + + def save_state(self): + """ + Saves the state of the torrent view. + """ + if component.get('MainWindow').visible(): + ListView.save_state(self, 'torrentview.state') + + def remove_column(self, header): + """Removes the column with the name 'header' from the torrentview""" + self.save_state() + ListView.remove_column(self, header) + + def set_filter(self, filter_dict): + """ + Sets filters for the torrentview.. + + see: core.get_torrents_status + """ + search_filter = self.filter and self.filter.get('name', None) or None + self.filter = dict(filter_dict) # Copied version of filter_dict. + if search_filter and 'name' not in filter_dict: + self.filter['name'] = search_filter + self.update(select_row=True) + + def set_columns_to_update(self, columns=None): + status_keys = [] + self.columns_to_update = [] + + if columns is None: + # We need to iterate through all columns + columns = list(self.columns) + + # Iterate through supplied list of columns to update + for column in columns: + # Make sure column is visible and has 'status_field' set. + # If not, we can ignore it. + if ( + self.columns[column].column.get_visible() is True + and self.columns[column].hidden is False + and self.columns[column].status_field is not None + ): + for field in self.columns[column].status_field: + status_keys.append(field) + self.columns_to_update.append(column) + + # Remove duplicates + self.columns_to_update = list(set(self.columns_to_update)) + status_keys = list(set(status_keys + self.permanent_status_keys)) + return status_keys + + def send_status_request(self, columns=None, select_row=False): + # Store the 'status_fields' we need to send to core + status_keys = self.set_columns_to_update(columns) + + # If there is nothing in status_keys then we must not continue + if status_keys is []: + return + + # Remove duplicates from status_key list + status_keys = list(set(status_keys)) + + # Request the statuses for all these torrent_ids, this is async so we + # will deal with the return in a signal callback. + d = ( + component.get('SessionProxy') + .get_torrents_status(self.filter, status_keys) + .addCallback(self._on_get_torrents_status) + ) + if select_row: + d.addCallback(self.select_first_row) + + def select_first_row(self, ignored=None): + """ + Set the first row in the list selected if a selection does + not already exist + """ + rows = self.treeview.get_selection().get_selected_rows()[1] + # Only select row if noe rows are selected + if not rows: + self.treeview.get_selection().select_path((0,)) + + def update(self, select_row=False): + """ + Sends a status request to core and updates the torrent list with the result. + + :param select_row: if the first row in the list should be selected if + no rows are already selected. + :type select_row: boolean + + """ + if self.got_state: + if ( + self.search_box.search_pending is not None + and self.search_box.search_pending.active() + ): + # An update request is scheduled, let's wait for that one + return + # Send a status request + idle_add(self.send_status_request, None, select_row) + + def update_view(self, load_new_list=False): + """Update the torrent view model with data we've received.""" + filter_column = self.columns['filter'].column_indices[0] + status = self.status + + if not load_new_list: + # Freeze notications while updating + self.treeview.freeze_child_notify() + + # Get the columns to update from one of the torrents + if status: + torrent_id = list(status)[0] + fields_to_update = [] + for column in self.columns_to_update: + column_index = self.get_column_index(column) + for i, status_field in enumerate(self.columns[column].status_field): + # Only use columns that the torrent has in the state + if status_field in status[torrent_id]: + fields_to_update.append((column_index[i], status_field)) + + for row in self.liststore: + torrent_id = row[self.columns['torrent_id'].column_indices[0]] + # We expect the torrent_id to be in status and prev_status, + # as it will be as long as the list isn't changed by the user + + torrent_id_in_status = False + try: + torrent_status = status[torrent_id] + torrent_id_in_status = True + if torrent_status == self.prev_status[torrent_id]: + # The status dict is the same, so do nothing to update for this torrent + continue + except KeyError: + pass + + if not torrent_id_in_status: + if row[filter_column] is True: + row[filter_column] = False + else: + if row[filter_column] is False: + row[filter_column] = True + + # Find the fields to update + to_update = [] + for i, status_field in fields_to_update: + row_value = status[torrent_id][status_field] + if decode_bytes(row[i]) != row_value: + to_update.append(i) + to_update.append(row_value) + # Update fields in the liststore + if to_update: + self.liststore.set(row.iter, *to_update) + + if load_new_list: + # Create the model filter. This sets the model for the treeview and enables sorting. + self.create_model_filter() + else: + self.treeview.thaw_child_notify() + + component.get('MenuBar').update_menu() + self.prev_status = status + + def _on_get_torrents_status(self, status, select_row=False): + """Callback function for get_torrents_status(). 'status' should be a + dictionary of {torrent_id: {key, value}}.""" + self.status = status + if self.search_box.prefiltered is not None: + self.search_box.prefiltered = None + + if self.status == self.prev_status and self.prev_status: + # We do not bother updating since the status hasn't changed + self.prev_status = self.status + return + self.update_view() + + def add_rows(self, torrent_ids): + """Accepts a list of torrent_ids to add to self.liststore""" + torrent_id_column = self.columns['torrent_id'].column_indices[0] + dirty_column = self.columns['dirty'].column_indices[0] + filter_column = self.columns['filter'].column_indices[0] + for torrent_id in torrent_ids: + # Insert a new row to the liststore + row = self.liststore.append() + self.liststore.set( + row, + torrent_id_column, + torrent_id, + dirty_column, + True, + filter_column, + True, + ) + + def remove_row(self, torrent_id): + """Removes a row with torrent_id""" + for row in self.liststore: + if row[self.columns['torrent_id'].column_indices[0]] == torrent_id: + self.liststore.remove(row.iter) + # Force an update of the torrentview + self.update(select_row=True) + break + + def mark_dirty(self, torrent_id=None): + for row in self.liststore: + if ( + not torrent_id + or row[self.columns['torrent_id'].column_indices[0]] == torrent_id + ): + # log.debug('marking %s dirty', torrent_id) + row[self.columns['dirty'].column_indices[0]] = True + if torrent_id: + break + + def get_selected_torrent(self): + """Returns a torrent_id or None. If multiple torrents are selected, + it will return the torrent_id of the first one.""" + selected = self.get_selected_torrents() + if selected: + return selected[0] + else: + return selected + + def get_selected_torrents(self): + """Returns a list of selected torrents or None""" + torrent_ids = [] + try: + paths = self.treeview.get_selection().get_selected_rows()[1] + except AttributeError: + # paths is likely None .. so lets return [] + return [] + try: + for path in paths: + try: + row = self.treeview.get_model().get_iter(path) + except Exception as ex: + log.debug('Unable to get iter from path: %s', ex) + continue + + child_row = self.treeview.get_model().convert_iter_to_child_iter(row) + child_row = ( + self.treeview.get_model() + .get_model() + .convert_iter_to_child_iter(child_row) + ) + if self.liststore.iter_is_valid(child_row): + try: + value = self.liststore.get_value( + child_row, self.columns['torrent_id'].column_indices[0] + ) + except Exception as ex: + log.debug('Unable to get value from row: %s', ex) + else: + torrent_ids.append(value) + if len(torrent_ids) == 0: + return [] + + return torrent_ids + except (ValueError, TypeError): + return [] + + def get_torrent_status(self, torrent_id): + """Returns data stored in self.status, it may not be complete""" + try: + return self.status[torrent_id] + except KeyError: + return {} + + def get_visible_torrents(self): + return list(self.status) + + # Callbacks # + def on_button_press_event(self, widget, event): + """This is a callback for showing the right-click context menu.""" + log.debug('on_button_press_event') + # We only care about right-clicks + if event.button == 3 and event.window == self.treeview.get_bin_window(): + x, y = event.get_coords() + path = self.treeview.get_path_at_pos(int(x), int(y)) + if not path: + return + row = self.model_filter.get_iter(path[0]) + + if self.get_selected_torrents(): + if ( + self.model_filter.get_value( + row, self.columns['torrent_id'].column_indices[0] + ) + not in self.get_selected_torrents() + ): + self.treeview.get_selection().unselect_all() + self.treeview.get_selection().select_iter(row) + else: + self.treeview.get_selection().select_iter(row) + torrentmenu = component.get('MenuBar').torrentmenu + torrentmenu.popup(None, None, None, None, event.button, event.time) + return True + + def on_selection_changed(self, treeselection): + """This callback is know when the selection has changed.""" + log.debug('on_selection_changed') + component.get('TorrentDetails').update() + component.get('MenuBar').update_menu() + + def on_drag_drop(self, widget, drag_context, x, y, timestamp): + widget.stop_emission('drag-drop') + + def on_drag_data_received( + self, widget, drag_context, x, y, selection_data, info, timestamp + ): + widget.stop_emission('drag_data_received') + + def on_columns_changed_event(self, treeview): + log.debug('Treeview Columns Changed') + self.save_state() + + def on_torrentadded_event(self, torrent_id, from_state): + self.add_rows([torrent_id]) + self.update(select_row=True) + + def on_torrentremoved_event(self, torrent_id): + self.remove_row(torrent_id) + + def on_torrentstatechanged_event(self, torrent_id, state): + # Update the torrents state + for row in self.liststore: + if torrent_id != row[self.columns['torrent_id'].column_indices[0]]: + continue + + for name in self.columns_to_update: + if not self.columns[name].status_field: + continue + for idx, status_field in enumerate(self.columns[name].status_field): + # Update all columns that use the state field to current state + if status_field != 'state': + continue + row[self.get_column_index(name)[idx]] = state + + if self.filter.get('state', None) is not None: + # We have a filter set, let's see if theres anything to hide + # and remove from status + if ( + torrent_id in self.status + and self.status[torrent_id]['state'] != state + ): + row[self.columns['filter'].column_indices[0]] = False + del self.status[torrent_id] + + self.mark_dirty(torrent_id) + + def on_sessionpaused_event(self): + self.mark_dirty() + self.update() + + def on_sessionresumed_event(self): + self.mark_dirty() + self.update(select_row=True) + + def on_torrentqueuechanged_event(self): + self.mark_dirty() + self.update() + + # Handle keyboard shortcuts + def on_key_press_event(self, widget, event): + keyname = keyval_name(event.keyval) + if keyname is not None: + func = getattr(self, 'keypress_' + keyname.lower(), None) + if func: + return func(event) + + def keypress_up(self, event): + """Handle any Up arrow keypresses""" + log.debug('keypress_up') + torrents = self.get_selected_torrents() + if not torrents: + return + + # Move queue position up with Ctrl+Alt or Ctrl+Alt+Shift + if event.get_state() & CTRL_ALT_MASK: + if event.get_state() & ModifierType.SHIFT_MASK: + client.core.queue_top(torrents) + else: + client.core.queue_up(torrents) + + def keypress_down(self, event): + """Handle any Down arrow keypresses""" + log.debug('keypress_down') + torrents = self.get_selected_torrents() + if not torrents: + return + + # Move queue position down with Ctrl+Alt or Ctrl+Alt+Shift + if event.get_state() & CTRL_ALT_MASK: + if event.get_state() & ModifierType.SHIFT_MASK: + client.core.queue_bottom(torrents) + else: + client.core.queue_down(torrents) + + def keypress_delete(self, event): + log.debug('keypress_delete') + torrents = self.get_selected_torrents() + if torrents: + if event.get_state() & ModifierType.SHIFT_MASK: + RemoveTorrentDialog(torrents, delete_files=True).run() + else: + RemoveTorrentDialog(torrents).run() + + def keypress_menu(self, event): + log.debug('keypress_menu') + if not self.get_selected_torrent(): + return + + torrentmenu = component.get('MenuBar').torrentmenu + torrentmenu.popup(None, None, None, None, 3, event.time) + return True diff --git a/deluge/ui/gtk3/torrentview_data_funcs.py b/deluge/ui/gtk3/torrentview_data_funcs.py new file mode 100644 index 0000000..0b2545d --- /dev/null +++ b/deluge/ui/gtk3/torrentview_data_funcs.py @@ -0,0 +1,277 @@ +# +# Copyright (C) 2007, 2008 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. +# + +import warnings +from functools import partial + +import deluge.common as common +import deluge.component as component + +from .common import ( + create_blank_pixbuf, + get_pixbuf, + icon_alert, + icon_checking, + icon_downloading, + icon_inactive, + icon_queued, + icon_seeding, +) + +# Holds the info for which status icon to display based on TORRENT_STATE +ICON_STATE = { + 'Allocating': icon_checking, + 'Checking': icon_checking, + 'Downloading': icon_downloading, + 'Seeding': icon_seeding, + 'Paused': icon_inactive, + 'Error': icon_alert, + 'Queued': icon_queued, + 'Moving': icon_checking, +} + +# Cache the key used to calculate the current value set for the specific cell +# renderer. This is much cheaper than fetch the current value and test if +# it's equal. +func_last_value = { + 'cell_data_ratio_seeds_peers': None, + 'cell_data_ratio_ratio': None, + 'cell_data_ratio_avail': None, + 'cell_data_date_added': None, + 'cell_data_date_completed': None, + 'cell_data_date_or_never': None, + 'cell_data_speed_limit_down': None, + 'cell_data_speed_limit_up': None, + 'cell_data_trackericon': None, + 'cell_data_statusicon': None, + 'cell_data_queue': None, + 'cell_data_progress': [None, None], + 'cell_data_peer_progress': None, +} + + +def cell_data_statusicon(column, cell, model, row, data): + """Display text with an icon""" + try: + state = model.get_value(row, data) + + if func_last_value['cell_data_statusicon'] == state: + return + func_last_value['cell_data_statusicon'] = state + + icon = ICON_STATE[state] + + # Supress Warning: g_object_set_qdata: assertion `G_IS_OBJECT (object)' failed + original_filters = warnings.filters[:] + warnings.simplefilter('ignore') + try: + cell.set_property('pixbuf', icon) + finally: + warnings.filters = original_filters + + except KeyError: + pass + + +def set_tracker_icon(tracker_icon, cell): + if tracker_icon: + pixbuf = tracker_icon.get_cached_icon() + if pixbuf is None: + pixbuf = get_pixbuf(tracker_icon.get_filename(), 16) + tracker_icon.set_cached_icon(pixbuf) + else: + pixbuf = create_blank_pixbuf() + + # Suppress Warning: g_object_set_qdata: assertion `G_IS_OBJECT (object)' failed + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + cell.set_property('pixbuf', pixbuf) + + +def cell_data_trackericon(column, cell, model, row, data): + host = model[row][data] + + if func_last_value['cell_data_trackericon'] == host: + return + if host: + if not component.get('TrackerIcons').has(host): + # Set blank icon while waiting for the icon to be loaded + set_tracker_icon(None, cell) + component.get('TrackerIcons').fetch(host) + func_last_value['cell_data_trackericon'] = None + else: + set_tracker_icon(component.get('TrackerIcons').get(host), cell) + # Only set the last value when we have found the icon + func_last_value['cell_data_trackericon'] = host + else: + set_tracker_icon(None, cell) + func_last_value['cell_data_trackericon'] = None + + +def cell_data_progress(column, cell, model, row, data): + """Display progress bar with text""" + (value, state_str) = model.get(row, *data) + if func_last_value['cell_data_progress'][0] != value: + func_last_value['cell_data_progress'][0] = value + cell.set_property('value', value) + + # Marked for translate states text are in filtertreeview + textstr = _(state_str) + if state_str not in ('Error', 'Seeding') and value < 100: + textstr = '%s %i%%' % (textstr, value) + + if func_last_value['cell_data_progress'][1] != textstr: + func_last_value['cell_data_progress'][1] = textstr + cell.set_property('text', textstr) + + +def cell_data_peer_progress(column, cell, model, row, data): + value = model.get_value(row, data) * 100 + if func_last_value['cell_data_peer_progress'] != value: + func_last_value['cell_data_peer_progress'] = value + cell.set_property('value', value) + cell.set_property('text', '%i%%' % value) + + +def cell_data_queue(column, cell, model, row, data): + value = model.get_value(row, data) + + if func_last_value['cell_data_queue'] == value: + return + func_last_value['cell_data_queue'] = value + + if value < 0: + cell.set_property('text', '') + else: + cell.set_property('text', str(value + 1)) + + +def cell_data_speed(cell, model, row, data): + """Display value as a speed, eg. 2 KiB/s""" + speed = model.get_value(row, data) + + if speed > 0: + speed_str = common.fspeed(speed, shortform=True) + cell.set_property( + 'markup', '{} <small>{}</small>'.format(*tuple(speed_str.split())) + ) + else: + cell.set_property('text', '') + + +def cell_data_speed_down(column, cell, model, row, data): + """Display value as a speed, eg. 2 KiB/s""" + cell_data_speed(cell, model, row, data) + + +def cell_data_speed_up(column, cell, model, row, data): + """Display value as a speed, eg. 2 KiB/s""" + cell_data_speed(cell, model, row, data) + + +def cell_data_speed_limit(cell, model, row, data, cache_key): + """Display value as a speed, eg. 2 KiB/s""" + speed = model.get_value(row, data) + + if func_last_value[cache_key] == speed: + return + func_last_value[cache_key] = speed + + if speed > 0: + speed_str = common.fspeed(speed * 1024, shortform=True) + cell.set_property( + 'markup', '{} <small>{}</small>'.format(*tuple(speed_str.split())) + ) + else: + cell.set_property('text', '') + + +def cell_data_speed_limit_down(column, cell, model, row, data): + cell_data_speed_limit(cell, model, row, data, 'cell_data_speed_limit_down') + + +def cell_data_speed_limit_up(column, cell, model, row, data): + cell_data_speed_limit(cell, model, row, data, 'cell_data_speed_limit_up') + + +def cell_data_size(column, cell, model, row, data): + """Display value in terms of size, eg. 2 MB""" + size = model.get_value(row, data) + cell.set_property('text', common.fsize(size, shortform=True)) + + +def cell_data_peer(column, cell, model, row, data): + """Display values as 'value1 (value2)'""" + (first, second) = model.get(row, *data) + # Only display a (total) if second is greater than -1 + if second > -1: + cell.set_property('text', '%d (%d)' % (first, second)) + else: + cell.set_property('text', '%d' % first) + + +def cell_data_time(column, cell, model, row, data): + """Display value as time, eg 1m10s""" + time = model.get_value(row, data) + if time <= 0: + time_str = '' + else: + time_str = common.ftime(time) + cell.set_property('text', time_str) + + +def cell_data_ratio(cell, model, row, data, cache_key): + """Display value as a ratio with a precision of 2.""" + ratio = model.get_value(row, data) + # Previous value in cell is the same as for this value, so ignore + if func_last_value[cache_key] == ratio: + return + func_last_value[cache_key] = ratio + cell.set_property( + 'text', '∞' if ratio < 0 else ('%.1f' % ratio).rstrip('0').rstrip('.') + ) + + +def cell_data_ratio_seeds_peers(column, cell, model, row, data): + cell_data_ratio(cell, model, row, data, 'cell_data_ratio_seeds_peers') + + +def cell_data_ratio_ratio(column, cell, model, row, data): + cell_data_ratio(cell, model, row, data, 'cell_data_ratio_ratio') + + +def cell_data_ratio_avail(column, cell, model, row, data): + cell_data_ratio(cell, model, row, data, 'cell_data_ratio_avail') + + +def cell_data_date(column, cell, model, row, data, key): + """Display value as date, eg 05/05/08""" + date = model.get_value(row, data) + + if func_last_value[key] == date: + return + func_last_value[key] = date + + date_str = common.fdate(date, date_only=True) if date > 0 else '' + cell.set_property('text', date_str) + + +cell_data_date_added = partial(cell_data_date, key='cell_data_date_added') +cell_data_date_completed = partial(cell_data_date, key='cell_data_date_completed') + + +def cell_data_date_or_never(column, cell, model, row, data): + """Display value as date, eg 05/05/08 or Never""" + value = model.get_value(row, data) + + if func_last_value['cell_data_date_or_never'] == value: + return + func_last_value['cell_data_date_or_never'] = value + + date_str = common.fdate(value, date_only=True) if value > 0 else _('Never') + cell.set_property('text', date_str) diff --git a/deluge/ui/gtk3/trackers_tab.py b/deluge/ui/gtk3/trackers_tab.py new file mode 100644 index 0000000..d671471 --- /dev/null +++ b/deluge/ui/gtk3/trackers_tab.py @@ -0,0 +1,69 @@ +# +# Copyright (C) 2008 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. +# + +import logging + +import deluge.component as component +from deluge.common import ftime + +from .tab_data_funcs import fcount, ftranslate, fyes_no +from .torrentdetails import Tab + +log = logging.getLogger(__name__) + + +class TrackersTab(Tab): + def __init__(self): + super().__init__('Trackers', 'trackers_tab', 'trackers_tab_label') + + self.add_tab_widget('summary_next_announce', ftime, ('next_announce',)) + self.add_tab_widget('summary_tracker', None, ('tracker_host',)) + self.add_tab_widget('summary_tracker_status', ftranslate, ('tracker_status',)) + self.add_tab_widget('summary_tracker_total', fcount, ('trackers',)) + self.add_tab_widget('summary_private', fyes_no, ('private',)) + + component.get('MainWindow').connect_signals(self) + + def update(self): + # Get the first selected torrent + selected = component.get('TorrentView').get_selected_torrents() + + # Only use the first torrent in the list or return if None selected + if selected: + selected = selected[0] + else: + self.clear() + return + + session = component.get('SessionProxy') + session.get_torrent_status(selected, self.status_keys).addCallback( + self._on_get_torrent_status + ) + + def _on_get_torrent_status(self, status): + # Check to see if we got valid data from the core + if not status: + return + + # Update all the tab label widgets + for widget in self.tab_widgets.values(): + txt = self.widget_status_as_fstr(widget, status) + if widget.obj.get_text() != txt: + widget.obj.set_text(txt) + + def clear(self): + for widget in self.tab_widgets.values(): + widget.obj.set_text('') + + def on_button_edit_trackers_clicked(self, button): + torrent_id = component.get('TorrentView').get_selected_torrent() + if torrent_id: + from .edittrackersdialog import EditTrackersDialog + + dialog = EditTrackersDialog(torrent_id, component.get('MainWindow').window) + dialog.run() |