From 1596475db23e66d0d54fb927113a4850b95efc9d Mon Sep 17 00:00:00 2001 From: bendikro Date: Fri, 17 May 2013 19:06:00 +0100 Subject: GTKUI: New path chooser to handle remote paths and store favorite paths --- deluge/core/core.py | 8 + deluge/core/preferencesmanager.py | 7 + deluge/path_chooser_common.py | 103 ++ deluge/ui/gtkui/addtorrentdialog.py | 84 +- deluge/ui/gtkui/connectionmanager.py | 1 - deluge/ui/gtkui/glade/add_torrent_dialog.ui | 216 ++-- deluge/ui/gtkui/glade/main_window.tabs.ui | 440 ++++---- deluge/ui/gtkui/glade/move_storage_dialog.ui | 20 +- deluge/ui/gtkui/glade/path_combo_chooser.ui | 947 ++++++++++++++++ deluge/ui/gtkui/glade/preferences_dialog.ui | 91 +- deluge/ui/gtkui/menubar.py | 45 +- deluge/ui/gtkui/options_tab.py | 35 +- deluge/ui/gtkui/path_chooser.py | 191 ++++ deluge/ui/gtkui/path_combo_chooser.py | 1543 ++++++++++++++++++++++++++ deluge/ui/gtkui/preferences.py | 431 +++---- 15 files changed, 3389 insertions(+), 773 deletions(-) create mode 100644 deluge/path_chooser_common.py create mode 100644 deluge/ui/gtkui/glade/path_combo_chooser.ui create mode 100644 deluge/ui/gtkui/path_chooser.py create mode 100755 deluge/ui/gtkui/path_combo_chooser.py diff --git a/deluge/core/core.py b/deluge/core/core.py index dd8a76b2b..710993ad3 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -48,6 +48,7 @@ import twisted.web.client import twisted.web.error from deluge.httpdownloader import download_file +from deluge import path_chooser_common import deluge.configmanager import deluge.common @@ -858,6 +859,13 @@ class Core(component.Component): """ return lt.version + @export + def get_completion_paths(self, value, hidden_files=False): + """ + Returns the available path completions for the input value. + """ + return path_chooser_common.get_completion_paths(value, hidden_files) + @export(AUTH_LEVEL_ADMIN) def get_known_accounts(self): return self.authmanager.get_known_accounts() diff --git a/deluge/core/preferencesmanager.py b/deluge/core/preferencesmanager.py index a085d2c99..70d6d7113 100644 --- a/deluge/core/preferencesmanager.py +++ b/deluge/core/preferencesmanager.py @@ -101,6 +101,13 @@ DEFAULT_PREFS = { "auto_managed": True, "move_completed": False, "move_completed_path": deluge.common.get_default_download_dir(), + "move_completed_paths_list": [], + "download_location_paths_list": [], + "path_chooser_show_chooser_button_on_localhost": True, + "path_chooser_auto_complete_enabled": True, + "path_chooser_accelerator_string": "Tab", + "path_chooser_max_popup_rows": 20, + "path_chooser_show_hidden_files": False, "new_release_check": True, "proxies": { "peer": { diff --git a/deluge/path_chooser_common.py b/deluge/path_chooser_common.py new file mode 100644 index 000000000..f44d1fbd0 --- /dev/null +++ b/deluge/path_chooser_common.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# path_chooser_common.py +# +# Copyright (C) 2013 Bro +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# + +import os + +def get_resource(filename): + import deluge + return deluge.common.resource_filename("deluge.ui.gtkui", os.path.join("glade", filename)) + +def is_hidden(filepath): + def has_hidden_attribute(filepath): + import win32api, win32con + try: + attribute = win32api.GetFileAttributes(filepath) + return attribute & (win32con.FILE_ATTRIBUTE_HIDDEN | win32con.FILE_ATTRIBUTE_SYSTEM) + except (AttributeError, AssertionError): + return False + + name = os.path.basename(os.path.abspath(filepath)) + # Windows + if os.name== 'nt': + return has_hidden_attribute(filepath) + return name.startswith('.') + +def get_completion_paths(path_value, hidden_files=False): + """ + Takes a path value and returns the available completions. + If the path_value is a valid path, return all sub-directories. + If the path_value is not a valid path, remove the basename from the + path and return all sub-directories of path that start with basename. + + :param path_value: path to complete + :type path_value: string + :returns: a sorted list of available completions for the input value + :rtype: list + + """ + def get_subdirs(dirname): + try: + return os.walk(dirname).next()[1] + except StopIteration: + # Invalid dirname + return [] + + dirname = os.path.dirname(path_value) + basename = os.path.basename(path_value) + + dirs = get_subdirs(dirname) + # No completions available + if not dirs: + return [] + + # path_value ends with path separator so + # we only want all the subdirectories + if not basename: + # Lets remove hidden files + if not hidden_files: + old_dirs = dirs + dirs = [] + for d in old_dirs: + if not is_hidden(os.path.join(dirname, d)): + dirs.append(d) + matching_dirs = [] + for s in dirs: + if s.startswith(basename): + p = os.path.join(dirname, s) + if not p.endswith(os.path.sep): + p += os.path.sep + matching_dirs.append(p) + return sorted(matching_dirs) diff --git a/deluge/ui/gtkui/addtorrentdialog.py b/deluge/ui/gtkui/addtorrentdialog.py index 13ac640d6..dd8663233 100644 --- a/deluge/ui/gtkui/addtorrentdialog.py +++ b/deluge/ui/gtkui/addtorrentdialog.py @@ -55,6 +55,8 @@ import deluge.ui.common import dialogs import common +from deluge.ui.gtkui.path_chooser import PathChooser + log = logging.getLogger(__name__) class AddTorrentDialog(component.Component): @@ -144,6 +146,9 @@ class AddTorrentDialog(component.Component): self.listview_torrents.get_selection().connect("changed", self._on_torrent_changed) + self.setup_move_completed_path_chooser() + self.setup_download_location_path_chooser() + # Get default config values from the core self.core_keys = [ "compact_allocation", @@ -153,13 +158,15 @@ class AddTorrentDialog(component.Component): "max_download_speed_per_torrent", "prioritize_first_last_pieces", "sequential_download", - "download_location", "add_paused", + "download_location", + "download_location_paths_list", "move_completed", - "move_completed_path" + "move_completed_path", + "move_completed_paths_list", ] + #self.core_keys += self.move_completed_path_chooser.get_config_keys() self.core_config = {} - self.builder.get_object("notebook1").connect("switch-page", self._on_switch_page) def start(self): @@ -169,17 +176,6 @@ class AddTorrentDialog(component.Component): return self.update_core_config(True, focus) def _show(self, focus=False): - if client.is_localhost(): - self.builder.get_object("button_location").show() - self.builder.get_object("entry_download_path").hide() - self.builder.get_object("button_move_completed_location").show() - self.builder.get_object("entry_move_completed_path").hide() - else: - self.builder.get_object("button_location").hide() - self.builder.get_object("entry_download_path").show() - self.builder.get_object("button_move_completed_location").hide() - self.builder.get_object("entry_move_completed_path").show() - if component.get("MainWindow").is_on_active_workspace(): self.dialog.set_transient_for(component.get("MainWindow").window) else: @@ -374,6 +370,23 @@ class AddTorrentDialog(component.Component): ret += value[1]["size"] 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"]) + + 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") + 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") + 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() @@ -381,16 +394,8 @@ class AddTorrentDialog(component.Component): options = self.options[torrent_id] - if client.is_localhost(): - self.builder.get_object("button_location").set_current_folder( - options["download_location"]) - self.builder.get_object("button_move_completed_location").set_current_folder( - options["move_completed_path"]) - else: - self.builder.get_object("entry_download_path").set_text( - options["download_location"]) - self.builder.get_object("entry_move_completed_path").set_text( - options["move_completed_path"]) + 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("radio_full").set_active( not options["compact_allocation"]) @@ -430,18 +435,10 @@ class AddTorrentDialog(component.Component): else: options = {} - if client.is_localhost(): - options["download_location"] = \ - self.builder.get_object("button_location").get_filename() - options["move_completed_path"] = \ - self.builder.get_object("button_move_completed_location").get_filename() - else: - options["download_location"] = \ - self.builder.get_object("entry_download_path").get_text() - options["move_completed_path"] = \ - self.builder.get_object("entry_move_completed_path").get_text() - options["compact_allocation"] = \ - self.builder.get_object("radio_compact").get_active() + options["download_location"] = self.download_location_path_chooser.get_text() + options["move_completed_path"] = self.move_completed_path_chooser.get_text() + options["compact_allocation"] = self.builder.get_object("radio_compact").get_active() + options["move_completed"] = self.builder.get_object("chk_move_completed").get_active() if options["compact_allocation"]: # We need to make sure all the files are set to download @@ -491,16 +488,7 @@ class AddTorrentDialog(component.Component): return priorities def set_default_options(self): - if client.is_localhost(): - self.builder.get_object("button_location").set_current_folder( - self.core_config["download_location"]) - self.builder.get_object("button_move_completed_location").set_current_folder( - self.core_config["move_completed_path"]) - else: - self.builder.get_object("entry_download_path").set_text( - self.core_config["download_location"]) - self.builder.get_object("entry_move_completed_path").set_text( - self.core_config["move_completed_path"]) + self.load_path_choosers_data() self.builder.get_object("radio_compact").set_active( self.core_config["compact_allocation"]) @@ -814,7 +802,6 @@ class AddTorrentDialog(component.Component): options) row = self.torrent_liststore.iter_next(row) - self.hide() def _on_button_apply_clicked(self, widget): @@ -847,8 +834,7 @@ class AddTorrentDialog(component.Component): def _on_chk_move_completed_toggled(self, widget): value = widget.get_active() - self.builder.get_object("button_move_completed_location").set_sensitive(value) - self.builder.get_object("entry_move_completed_path").set_sensitive(value) + self.move_completed_path_chooser.set_sensitive(value) def _on_delete_event(self, widget, event): self.hide() diff --git a/deluge/ui/gtkui/connectionmanager.py b/deluge/ui/gtkui/connectionmanager.py index 28eab091a..f60cdfc85 100644 --- a/deluge/ui/gtkui/connectionmanager.py +++ b/deluge/ui/gtkui/connectionmanager.py @@ -311,7 +311,6 @@ class ConnectionManager(component.Component): # Return if the deferred callback was done after the dialog was closed if not self.running: return - row = self.__get_host_row(host_id) def on_info(info, c): if not self.running: diff --git a/deluge/ui/gtkui/glade/add_torrent_dialog.ui b/deluge/ui/gtkui/glade/add_torrent_dialog.ui index e7c85d3d5..4a4b101bb 100644 --- a/deluge/ui/gtkui/glade/add_torrent_dialog.ui +++ b/deluge/ui/gtkui/glade/add_torrent_dialog.ui @@ -1,6 +1,6 @@ - + - + -1 @@ -27,6 +27,7 @@ 10 + False 5 Add Torrents center-on-parent @@ -35,6 +36,7 @@ True + False 2 @@ -43,17 +45,20 @@ True + False 0 none True + False 5 12 12 True + False True @@ -71,27 +76,32 @@ + True + True 0 True + False center True True True - + True + False 2 4 True + False gtk-open 1 @@ -104,6 +114,7 @@ True + False _File True @@ -127,15 +138,17 @@ True True True - + True + False 2 4 True + False gtk-network 1 @@ -148,6 +161,7 @@ True + False _URL True @@ -171,15 +185,17 @@ True True True - + True + False 2 4 True + False gtk-revert-to-saved 1 @@ -192,6 +208,7 @@ True + False Info_hash True @@ -215,14 +232,16 @@ True True True - + True + False 4 True + False gtk-remove @@ -234,6 +253,7 @@ True + False _Remove True @@ -266,6 +286,7 @@ True + False Torrents @@ -304,22 +325,29 @@ True + False True + False gtk-open + True + True 0 True + False Fi_les True + True + True 5 1 @@ -332,41 +360,28 @@ True + False 5 5 True + False 0 none True + False 5 5 5 - + True + False - - select-folder - Select A Folder - - - 0 - - - - - True - True - True - True - - - 1 - + @@ -375,6 +390,7 @@ True + False Download Location @@ -391,53 +407,36 @@ True + False 0 none True + False 5 5 5 - + True + False True True False True - + False + True 0 - - False - True - select-folder - Select A Folder - - - 1 - - - - - False - True - - True - True - True - - - 2 - + @@ -446,6 +445,7 @@ True + False Move Complete Location @@ -462,20 +462,24 @@ True + False 10 True + False 0 none True + False 5 12 True + False Full @@ -484,7 +488,7 @@ False True True - + False @@ -500,7 +504,7 @@ False True radio_full - + False @@ -515,6 +519,7 @@ True + False Allocation @@ -531,16 +536,19 @@ True + False 0 none True + False 5 12 True + False 4 2 10 @@ -549,6 +557,8 @@ True True 1 + False + False True True adjustment1 @@ -556,24 +566,26 @@ 1 2 - - + + True + False 0 Max Down Speed: GTK_FILL - + True + False 0 Max Up Speed: @@ -581,12 +593,13 @@ 1 2 GTK_FILL - + True + False 0 Max Connections: @@ -594,12 +607,13 @@ 2 3 GTK_FILL - + True + False 0 Max Upload Slots: @@ -607,7 +621,7 @@ 3 4 GTK_FILL - + @@ -615,6 +629,8 @@ True True 1 + False + False True True adjustment2 @@ -625,8 +641,8 @@ 2 1 2 - - + + @@ -634,6 +650,8 @@ True True 1 + False + False True True adjustment3 @@ -643,8 +661,8 @@ 2 2 3 - - + + @@ -652,6 +670,8 @@ True True 1 + False + False True True adjustment4 @@ -661,8 +681,8 @@ 2 3 4 - - + + @@ -672,6 +692,7 @@ True + False Bandwidth @@ -688,16 +709,19 @@ True + False 0 none True + False 5 12 True + False Prioritize First/Last Pieces @@ -755,6 +779,7 @@ used sparingly. True + False General @@ -778,23 +803,28 @@ used sparingly. True + False 5 - + True + False - + True True True - + - + True + False - + True - gtk-revert-to-saved + False + 1 + gtk-apply False @@ -803,9 +833,11 @@ used sparingly. - + True - Revert To Defaults + False + 0 + Apply To All False @@ -823,26 +855,28 @@ used sparingly. False False end - 1 + 0 - + True + False - + True True True - + - + True + False - + True - 1 - gtk-apply + False + gtk-revert-to-saved False @@ -851,10 +885,10 @@ used sparingly. - + True - 0 - Apply To All + False + Revert To Defaults False @@ -872,7 +906,7 @@ used sparingly. False False end - 0 + 1 @@ -890,22 +924,29 @@ used sparingly. True + False True + False gtk-properties + True + True 0 True + False _Options True + True + True 5 1 @@ -924,12 +965,15 @@ used sparingly. + True + True 0 True + False end @@ -938,7 +982,7 @@ used sparingly. True True True - + False @@ -953,7 +997,7 @@ used sparingly. True True True - + False @@ -964,6 +1008,7 @@ used sparingly. False + True end 1 @@ -975,4 +1020,5 @@ used sparingly. button_add + diff --git a/deluge/ui/gtkui/glade/main_window.tabs.ui b/deluge/ui/gtkui/glade/main_window.tabs.ui index 4b38cbbcb..e474cd865 100644 --- a/deluge/ui/gtkui/glade/main_window.tabs.ui +++ b/deluge/ui/gtkui/glade/main_window.tabs.ui @@ -804,6 +804,30 @@ 4 5 2 + + + + + + + + + + + + + + + + + + + + + + + + True @@ -817,7 +841,7 @@ 4 5 6 - + @@ -835,7 +859,7 @@ 5 6 GTK_FILL - + @@ -850,7 +874,7 @@ 2 4 5 - + @@ -868,7 +892,7 @@ 4 5 GTK_FILL - + @@ -885,7 +909,7 @@ 4 1 2 - + @@ -902,7 +926,7 @@ 1 2 GTK_FILL - + @@ -918,7 +942,7 @@ 4 7 8 - + @@ -936,7 +960,7 @@ 7 8 GTK_FILL - + @@ -961,7 +985,7 @@ 3 4 GTK_FILL - + @@ -976,7 +1000,7 @@ 1 4 - + @@ -1000,7 +1024,7 @@ GTK_FILL - + @@ -1024,7 +1048,7 @@ 2 3 GTK_FILL - + @@ -1041,7 +1065,7 @@ 4 2 3 - + @@ -1059,7 +1083,7 @@ 6 7 GTK_FILL - + @@ -1074,7 +1098,7 @@ 4 6 7 - + @@ -1089,7 +1113,7 @@ 2 3 4 - + @@ -1107,7 +1131,7 @@ 8 9 GTK_FILL - + @@ -1125,7 +1149,7 @@ 9 10 GTK_FILL - + @@ -1141,7 +1165,7 @@ 2 8 9 - + @@ -1157,33 +1181,9 @@ 2 9 10 - + - - - - - - - - - - - - - - - - - - - - - - - - @@ -1351,9 +1351,10 @@ queue none - + True False + 3 True @@ -1370,10 +1371,16 @@ True False 5 - 5 + 4 3 5 2 + + + + + + True @@ -1394,8 +1401,8 @@ 2 2 3 - - + + @@ -1419,8 +1426,8 @@ 2 1 2 - - + + @@ -1430,6 +1437,7 @@ 6 1 + True False False True @@ -1442,8 +1450,8 @@ 1 2 - - + + @@ -1457,7 +1465,7 @@ 2 3 GTK_FILL - + @@ -1471,7 +1479,7 @@ 1 2 GTK_FILL - + @@ -1483,7 +1491,7 @@ GTK_FILL - + @@ -1495,8 +1503,8 @@ 2 3 - - + + @@ -1510,8 +1518,8 @@ 3 1 2 - - + + @@ -1525,7 +1533,7 @@ 3 4 GTK_FILL - + @@ -1548,25 +1556,10 @@ 2 3 4 - - + + - - - - - - - - - - - - - - - @@ -1583,9 +1576,8 @@ - False - False - 0 + + GTK_FILL @@ -1610,7 +1602,6 @@ True True False - False True @@ -1635,7 +1626,6 @@ True True False - False True @@ -1684,7 +1674,6 @@ True True False - False True @@ -1696,67 +1685,6 @@ 1 - - - Move completed: - True - True - False - False - True - - - - False - False - 2 - - - - - True - False - - - True - False - False - select-folder - False - True - Select A Folder - - - False - False - 0 - - - - - False - True - - True - True - False - False - True - True - - - False - False - 1 - - - - - False - False - 3 - - True @@ -1780,15 +1708,18 @@ - False - False - 1 + 1 + 2 + + GTK_FILL - + True False + 2 + 2 True @@ -1804,9 +1735,11 @@ 5 12 - + True False + 3 + 2 Private @@ -1814,14 +1747,26 @@ False True False - False True - False - False - 0 + + + + + + Shared + True + True + False + True + + + + 1 + 2 + @@ -1830,14 +1775,14 @@ True True False - False True - False - False - 1 + 2 + 1 + 2 + @@ -1846,60 +1791,148 @@ True True False - False True - True - True - 2 + 2 + 2 + 3 + + + + + + + + True + False + <b>General</b> + True + + + + + GTK_EXPAND | GTK_SHRINK | GTK_FILL + + + + + True + False + 0 + none + + + True + False + 12 + 5 + + + True + False + 2 - - Shared + + Move completed: True True False - False True - + - True - True - 3 + - + + True + False + + + + + + 1 + 2 + + + + + + + + + + + + + 2 + 1 + 2 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + gtk-apply + True + False + True + True + True + + + + False + False + 0 + + + + True True True - False 0 0 - - + True False 5 - + True False gtk-edit False - False + True 0 - + True False _Edit Trackers @@ -1917,15 +1950,18 @@ False False - 4 + 1 + + + - + True False General @@ -1936,53 +1972,17 @@ - False - False - 0 - - - - - True - False - 0 - none - - - True - False - 0 - 0 - 12 - - - gtk-apply - True - False - True - True - False - True - - - - - - - - - - - False - False - 1 + 1 + 2 + + GTK_FILL - False - False - 2 + 2 + 3 + diff --git a/deluge/ui/gtkui/glade/move_storage_dialog.ui b/deluge/ui/gtkui/glade/move_storage_dialog.ui index cf505b751..30d633751 100644 --- a/deluge/ui/gtkui/glade/move_storage_dialog.ui +++ b/deluge/ui/gtkui/glade/move_storage_dialog.ui @@ -116,7 +116,7 @@ - + True False 5 @@ -133,23 +133,7 @@ - - True - True - True - True - True - True - False - False - True - True - - - True - True - 1 - + diff --git a/deluge/ui/gtkui/glade/path_combo_chooser.ui b/deluge/ui/gtkui/glade/path_combo_chooser.ui new file mode 100644 index 000000000..330579f69 --- /dev/null +++ b/deluge/ui/gtkui/glade/path_combo_chooser.ui @@ -0,0 +1,947 @@ + + + + + + + 100 + 1 + 10 + + + 100 + 1 + 10 + + + -1 + 100 + 1 + 10 + + + False + 5 + Properties + True + True + dialog + + + + + True + False + 5 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 3 + 2 + 5 + + + + + + Show file chooser + True + True + False + True + + + + 1 + 2 + + + + + True + False + Max drop down rows + + + + + True + True + 2 + + True + False + False + True + True + adjustment3 + 1 + True + + + + 1 + 2 + GTK_EXPAND + + + + + Show path entry + True + True + False + True + + + + 2 + 3 + + + + + Show folder name + True + True + False + True + + + + 1 + 2 + 1 + 2 + + + + + + + + + True + False + <b>General</b> + True + + + + + True + True + 0 + + + + + True + False + end + + + + + + Close + True + True + True + + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + 0 + + + True + False + 12 + + + True + False + 2 + 2 + 5 + 5 + + + + + + Enable auto completion + True + True + False + True + + + + + + Set new key + True + True + True + Press this key to set new key accelerators to trigger auto-complete + + + + + 1 + 2 + + + + + Show hidden files + True + True + False + 0.55000001192092896 + True + + + + 1 + 2 + + + + + + + + + True + False + <b>Auto completion</b> + True + + + + + True + True + 2 + + + + + True + False + 0 + etched-out + + + True + False + 5 + 7 + 23 + + + True + False + 6 + 2 + 2 + + + True + False + 0 + Auto-complete accelerator + + + + + True + False + 0 + Save selected entry + + + 1 + 2 + + + + + True + False + 0 + Edit selected entry + + + 2 + 3 + + + + + True + False + 0 + 0.019999999552965164 + Remove selected entry + + + 3 + 4 + + + + + True + False + 0 + Toggle display hidden files + + + 4 + 5 + + + + + True + False + CTRL-s + + + 1 + 2 + 1 + 2 + + + + + True + False + + + 1 + 2 + + + + + True + False + CTRL-e + + + 1 + 2 + 2 + 3 + + + + + True + False + CTRL-r + + + 1 + 2 + 3 + 4 + + + + + True + False + CTRL-h + + + 1 + 2 + 4 + 5 + + + + + True + False + 0 + Set default text in entry + + + 5 + 6 + + + + + True + False + CTRL-d + + + 1 + 2 + 5 + 6 + + + + + + + + + True + False + <b>Short cuts</b> + True + + + + + True + True + 3 + + + + + + config_dialog_button_close + + + + + + + + + + False + popup + False + True + combo + True + True + False + False + + + + + + True + False + 3 + 1 + + + True + True + True + True + True + automatic + automatic + in + + + True + True + completion_tree_store + False + False + False + 0 + False + + + + + + + autosize + 129 + column + True + + + + 0 + + + + + + + + + True + True + 0 + + + + + + + False + 5 + GtkFileChooserDialog + True + dialog + select-folder + False + + + True + False + 2 + + + True + False + end + + + Cancel + True + True + True + + + False + False + 0 + + + + + Open + True + True + True + + + False + False + 1 + + + + + False + True + end + 0 + + + + + + + + + filechooser_button_cancel + filechooser_button_open + + + + + + + + + + False + popup + False + True + popup-menu + True + True + False + False + + + + + + True + False + 3 + 2 + + + True + True + True + True + True + never + automatic + in + + + True + True + stored_values_tree_store + False + False + 0 + False + + + + + + + autosize + 127 + column + True + + + + + + 0 + + + + + + + + + True + True + 0 + + + + + True + False + + + True + False + 1 + start + + + + Add + True + True + True + Add the current entry value to the list + + + + False + False + 0 + + + + + Edit + True + True + True + Edit the selected entry + + + + False + False + 1 + + + + + Remove + True + True + True + Remove the selected entry + + + + False + False + 2 + + + + + Up + True + True + True + Move the selected entry up + + + + False + False + 3 + + + + + Down + True + True + True + Move the selected entry down + + + + False + False + 4 + + + + + Default + True + False + True + True + No default path set + + + + False + False + 5 + + + + + True + True + True + Open properties dialog + + + + True + False + gtk-properties + + + + + False + False + 6 + + + + + False + False + 2 + 0 + + + + + + + + False + False + 1 + + + + + + + False + + + True + False + 3 + + + + 160 + False + True + select-folder + False + False + Select a Directory + + + False + False + 0 + + + + + True + True + True + 0.50999999046325684 + + + + True + False + + + True + False + 0 + 1 + gtk-open + + + False + False + 0 + + + + + True + False + 0 + 6 + + + False + False + 1 + + + + + + + False + False + 1 + + + + + True + True + + True + False + False + True + True + + + + + + + + True + True + 2 + + + + + True + True + True + Saved paths + False + + + + + + 15 + True + False + True + + + True + False + up + + + True + False + 0 + + + + + True + False + down + + + True + False + 1 + + + + + + + False + False + 3 + + + + + + diff --git a/deluge/ui/gtkui/glade/preferences_dialog.ui b/deluge/ui/gtkui/glade/preferences_dialog.ui index 2d2541d49..a774e29a0 100644 --- a/deluge/ui/gtkui/glade/preferences_dialog.ui +++ b/deluge/ui/gtkui/glade/preferences_dialog.ui @@ -1307,38 +1307,12 @@ status tab (<b>EXPERIMENTAL!!!</b>) - + True False 5 - - True - False - False - select-folder - Select A Folder - - - True - True - 0 - - - - - True - True - False - False - True - True - - - True - True - 1 - + @@ -1351,38 +1325,12 @@ status tab (<b>EXPERIMENTAL!!!</b>) True False - + True False 5 - - True - False - select-folder - Select A Folder - - - True - True - 0 - - - - - True - - True - False - False - True - True - - - True - True - 1 - + @@ -1399,37 +1347,12 @@ status tab (<b>EXPERIMENTAL!!!</b>) True False - + True False 5 - - True - False - select-folder - Select A Folder - - - True - True - 0 - - - - - True - True - False - False - True - True - - - True - True - 1 - + @@ -1913,7 +1836,7 @@ used sparingly. - + Test Active Port False True diff --git a/deluge/ui/gtkui/menubar.py b/deluge/ui/gtkui/menubar.py index aebab1466..4399af5a5 100644 --- a/deluge/ui/gtkui/menubar.py +++ b/deluge/ui/gtkui/menubar.py @@ -47,6 +47,7 @@ import deluge.common import common import dialogs from deluge.configmanager import ConfigManager +from deluge.ui.gtkui.path_chooser import PathChooser log = logging.getLogger(__name__) @@ -322,31 +323,9 @@ class MenuBar(component.Component): def on_menuitem_move_activate(self, data=None): log.debug("on_menuitem_move_activate") - if client.is_localhost(): - from deluge.configmanager import ConfigManager - config = ConfigManager("gtkui.conf") - chooser = gtk.FileChooserDialog( - _("Choose a directory to move files to"), - component.get("MainWindow").window, - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, - buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_OK, gtk.RESPONSE_OK) - ) - chooser.set_local_only(True) - if not deluge.common.windows_check(): - chooser.set_icon(common.get_deluge_icon()) - chooser.set_property("skip-taskbar-hint", True) - chooser.set_current_folder(config["choose_directory_dialog_path"]) - if chooser.run() == gtk.RESPONSE_OK: - result = chooser.get_filename() - config["choose_directory_dialog_path"] = result - client.core.move_storage( - component.get("TorrentView").get_selected_torrents(), result) - chooser.destroy() - else: - component.get("SessionProxy").get_torrent_status( - component.get("TorrentView").get_selected_torrent(), - ["save_path"]).addCallback(self.show_move_storage_dialog) + component.get("SessionProxy").get_torrent_status( + component.get("TorrentView").get_selected_torrent(), + ["save_path"]).addCallback(self.show_move_storage_dialog) def show_move_storage_dialog(self, status): log.debug("show_move_storage_dialog") @@ -358,22 +337,26 @@ class MenuBar(component.Component): # 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.window.window) - self.move_storage_dialog_entry = builder.get_object("entry_destination") - self.move_storage_dialog_entry.set_text(status["save_path"]) - def on_dialog_response_event(widget, response_id): + self.move_storage_dialog_hbox = builder.get_object("hbox_entry") + self.move_storage_path_chooser = PathChooser("move_completed_paths_list") + 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["save_path"]) + def on_dialog_response_event(widget, response_id): def on_core_result(result): # Delete references del self.move_storage_dialog - del self.move_storage_dialog_entry + del self.move_storage_dialog_hbox if response_id == gtk.RESPONSE_OK: log.debug("Moving torrents to %s", - self.move_storage_dialog_entry.get_text()) - path = self.move_storage_dialog_entry.get_text() + 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_path_chooser.save_config() self.move_storage_dialog.hide() self.move_storage_dialog.connect("response", on_dialog_response_event) diff --git a/deluge/ui/gtkui/options_tab.py b/deluge/ui/gtkui/options_tab.py index 746d361f5..7ddb72903 100644 --- a/deluge/ui/gtkui/options_tab.py +++ b/deluge/ui/gtkui/options_tab.py @@ -37,6 +37,7 @@ import gtk.gdk import deluge.component as component from deluge.ui.client import client +from deluge.ui.gtkui.path_chooser import PathChooser from deluge.ui.gtkui.torrentdetails import Tab class OptionsTab(Tab): @@ -60,11 +61,15 @@ class OptionsTab(Tab): self.chk_remove_at_ratio = builder.get_object("chk_remove_at_ratio") self.spin_stop_ratio = builder.get_object("spin_stop_ratio") self.chk_move_completed = builder.get_object("chk_move_completed") - self.filechooser_move_completed = builder.get_object("filechooser_move_completed") self.entry_move_completed = builder.get_object("entry_move_completed") self.chk_shared = builder.get_object("chk_shared") self.button_apply = builder.get_object("button_apply") + self.move_completed_hbox = builder.get_object("hbox_move_completed_path_chooser") + self.move_completed_path_chooser = PathChooser("move_completed_paths_list") + self.move_completed_hbox.add(self.move_completed_path_chooser) + self.move_completed_hbox.show_all() + self.prev_torrent_id = None self.prev_status = None @@ -85,15 +90,7 @@ class OptionsTab(Tab): self.spin_stop_ratio.connect("key-press-event", self._on_key_press_event) def start(self): - if client.is_localhost(): - self.filechooser_move_completed.show() - self.entry_move_completed.hide() - else: - self.filechooser_move_completed.hide() - self.entry_move_completed.show() - self.entry_move_completed.connect( - "changed", self._on_entry_move_completed_changed - ) + pass def stop(self): pass @@ -169,10 +166,7 @@ class OptionsTab(Tab): if status["move_on_completed"] != self.prev_status["move_on_completed"]: self.chk_move_completed.set_active(status["move_on_completed"]) if status["move_on_completed_path"] != self.prev_status["move_on_completed_path"]: - if client.is_localhost(): - self.filechooser_move_completed.set_current_folder(status["move_on_completed_path"]) - else: - self.entry_move_completed.set_text(status["move_on_completed_path"]) + self.move_completed_path_chooser.set_text(status["move_on_completed_path"], cursor_end=False, default_text=True) if status["shared"] != self.prev_status["shared"]: self.chk_shared.set_active(status["shared"]) @@ -249,10 +243,7 @@ class OptionsTab(Tab): self.prev_torrent_id, self.chk_move_completed.get_active() ) if self.chk_move_completed.get_active(): - if client.is_localhost(): - path = self.filechooser_move_completed.get_filename() - else: - path = self.entry_move_completed.get_text() + path = self.move_completed_path_chooser.get_text() client.core.set_torrent_move_completed_path(self.prev_torrent_id, path) if self.chk_shared.get_active() != self.prev_status["shared"]: client.core.set_torrents_shared( @@ -274,13 +265,7 @@ class OptionsTab(Tab): def _on_chk_move_completed_toggled(self, widget): value = self.chk_move_completed.get_active() - if client.is_localhost(): - widget = self.filechooser_move_completed - else: - widget = self.entry_move_completed - - widget.set_sensitive(value) - + self.move_completed_path_chooser.set_sensitive(value) if not self.button_apply.is_sensitive(): self.button_apply.set_sensitive(True) diff --git a/deluge/ui/gtkui/path_chooser.py b/deluge/ui/gtkui/path_chooser.py new file mode 100644 index 000000000..533839c66 --- /dev/null +++ b/deluge/ui/gtkui/path_chooser.py @@ -0,0 +1,191 @@ +# +# path_chooser.py +# +# Copyright (C) 2013 Bro +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# + +import logging + +from deluge.ui.client import client +from deluge.ui.gtkui.path_combo_chooser import PathChooserComboBox +import deluge.component as component + +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 = self.config_keys_to_funcs_mapping.keys() + 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 not chooser.paths_config_key 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 not key is "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 = self.config_keys_to_funcs_mapping.keys() + keys += self.paths_list_keys + return keys + +class PathChooser(PathChooserComboBox): + + def __init__(self, paths_config_key=None): + self.paths_config_key = paths_config_key + PathChooserComboBox.__init__(self) + 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, e: + log.warn("TypeError: %s" % str(e)) + + # 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, value, hidden_files): + def on_paths_cb(paths): + self.complete(value, paths) + d = client.core.get_completion_paths(value, hidden_files=hidden_files) + d.addCallback(on_paths_cb) diff --git a/deluge/ui/gtkui/path_combo_chooser.py b/deluge/ui/gtkui/path_combo_chooser.py new file mode 100755 index 000000000..27a716f85 --- /dev/null +++ b/deluge/ui/gtkui/path_combo_chooser.py @@ -0,0 +1,1543 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# path_combo_chooser.py +# +# Copyright (C) 2013 Bro +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# + +import os + +import gobject +import gtk +from gtk import gdk, keysyms + +from deluge.path_chooser_common import get_resource, get_completion_paths + +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 == keysyms.Up or keyval == keysyms.KP_Up + +def key_is_down(keyval): + return keyval == keysyms.Down or keyval == keysyms.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 == keysyms.Page_Down or keyval == keysyms.Page_Up + +def key_is_enter(keyval): + return keyval == keysyms.Return or keyval == keysyms.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(object): + + 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: + 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 (type(paths) is list or type(paths) is 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.state & gtk.accelerator_get_default_mod_mask() + + if keyval == keysyms.Escape or\ + (key_is_up(keyval) and + state == gdk.MOD1_MASK): # ALT Key + 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 == gtk.gdk._2BUTTON_PRESS) or\ + (not double_click and event.type == gtk.gdk.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. None means no change. True means down + and False means up. + :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 not next is 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") + + # 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, focus_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(StoredValuesList, self).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("Edit path") + self.path_list_popup.append(menuitem_edit) + menuitem_remove = gtk.MenuItem("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, event.button, time, data=path) + 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(StoredValuesList, self).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 + state = event.state & gtk.accelerator_get_default_mod_mask() + ctrl = event.state & gtk.gdk.CONTROL_MASK + + # Edit selected row + if (keyval in [keysyms.Left, keysyms.Right, keysyms.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.state & gtk.gdk.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() + 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(CompletionList, self).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(CompletionList, self).on_value_list_treeview_key_press_event(widget, event) + if ret: + return ret + keyval = event.keyval + ctrl = event.state & gtk.gdk.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 + state = event.state + + path = self.treeview.get_path_at_pos(int(x), int(y)) + if path: + self.handle_list_scroll(path=path[0], next=None) + +class PathChooserPopup(object): + """ + + 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 + + def popup(self): + """ + Makes the popup visible. + + """ + # Entry is not yet visible + if not (self.path_entry.flags() & gtk.REALIZED): + return + if not self.is_popped_up(): + self.set_window_position_and_size() + + def popdown(self): + if not self.is_popped_up(): + return + if not (self.path_entry.flags() & gtk.REALIZED): + return + self.popup_window.grab_remove() + self.popup_window.hide_all() + + def is_popped_up(self): + """ + Return True if the window is popped up. + """ + return bool(self.popup_window.flags() & gtk.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. + + """ + self.popup_buttonbox = self.builder.get_object("buttonbox") + + # 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 + x, y = self.path_entry.window.get_origin() + + # Add the position of the alignment_widget relative to the parent window. + x += self.alignment_widget.allocation.x + y += self.alignment_widget.allocation.y + + height_extra = 8 + + height = self.popup_window.size_request()[1] + width = self.popup_window.size_request()[0] + + treeview_height = self.treeview.size_request()[1] + treeview_width = self.treeview.size_request()[0] + + if treeview_height > height: + height = treeview_height + height_extra + + butonbox_height = max(self.popup_buttonbox.size_request()[1], self.popup_buttonbox.allocation.height) + butonbox_width = max(self.popup_buttonbox.size_request()[0], self.popup_buttonbox.allocation.width) + + if treeview_height > butonbox_height and treeview_height < height : + height = treeview_height + height_extra + + # After removing an element from the tree store, self.treeview.size_request()[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 + butonbox_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 + + # Minimum width is the width of the path entry + width of buttonbox +# if width < self.alignment_widget.allocation.width + butonbox_width: +# width = self.alignment_widget.allocation.width + butonbox_width + + if width < self.alignment_widget.allocation.width: + width = self.alignment_widget.allocation.width + + # 10 is extra spacing + content_width = self.treeview.size_request()[0] + butonbox_width + 10 + + # If self.max_visible_rows is -1, not restriction is set + 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.size_request()[1] / len(self.tree_store) + # 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 + + # Minimum height is the height of the button box + if height < butonbox_height + height_extra: + height = butonbox_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.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.allocation.height + height <= monitor.y + monitor.height: + y += self.path_entry.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.allocation.height) > + y - monitor.y): + y += self.path_entry.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 = 0L + if gdk.pointer_grab(self.popup_window.window, True, + (gdk.BUTTON_PRESS_MASK | + gdk.BUTTON_RELEASE_MASK | + gdk.POINTER_MOTION_MASK), + None, None, activate_time) == 0: + if gdk.keyboard_grab(self.popup_window.window, True, activate_time) == 0: + return True + else: + self.popup_window.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) + if popdown: + self.popdown() + + def set_max_popup_rows(self, rows): + try: + int(rows) + except: + 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 + hide = False + # Also if the intersection of self and the event is empty, hide + # the path_list + if (tuple(self.popup_window.allocation.intersect( + gdk.Rectangle(x=int(event.x), y=int(event.y), + width=1, height=1))) == (0, 0, 0, 0)): + hide = True + # Toplevel is the window that received the event, and parent is the + # path_list window. If they are not the same, means the popup should + # be hidden. This is necessary for when the event happens on another + # widget + toplevel = event.window.get_toplevel() + parent = self.popup_window.window + + if toplevel != parent: + hide = True + if hide: + 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.popup_buttonbox = self.builder.get_object("buttonbox") + 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) + + # 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.flags() & gtk.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.state & gtk.gdk.CONTROL_MASK + self.handle_list_scroll(next=event.direction == gdk.SCROLL_DOWN, + set_entry=widget != self.treeview, swap=swap) + 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.state & gtk.accelerator_get_default_mod_mask() + if (keyval == keysyms.Escape or + (key_is_up(keyval) and + state == gdk.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) + +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.flags() & gtk.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())) + +################################################### +# 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, state = event.window.get_pointer() + self.handle_list_scroll(next=event.direction == gdk.SCROLL_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(object): + + 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(keysyms.Tab, 0) + + def on_entry_text_insert_text(self, entry, new_text, new_text_length, position): + if (self.path_entry.flags() & gtk.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) + 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): + """ + Remove the popup when characters are removed + + """ + if self.completion_popup.is_popped_up(): + self.completion_popup.popdown() + + def set_use_popup(self, use): + self.use_popup = use + + def on_completion_popup_window_key_press_event(self, entry, event): + """ + """ + # 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.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() + self.do_completion() + if values_count == 1: + self.completion_popup.popdown() + else: + #shift = event.state & gtk.gdk.SHIFT_MASK + #self.completion_popup.handle_list_scroll(next=False if shift else True) + self.completion_popup.handle_list_scroll(next=True) + return True + self.path_entry.text_entry.emit("key-press-event", event) + + def is_auto_completion_accelerator(self, keyval, state): + return gtk.accelerator_name(keyval, state.numerator) == self.accelerator_string + + def do_completion(self): + value = self.path_entry.get_text() + self.path_entry.text_entry.set_position(len(value)) + paths = self._start_completion(value, hidden_files=self.completion_popup.show_hidden_files) + + def _start_completion(self, value, hidden_files): + completion_paths = get_completion_paths(value, hidden_files) + self._end_completion(value, completion_paths) + + def _end_completion(self, value, paths): + common_prefix = os.path.commonprefix(paths) + if len(common_prefix) > len(value): + self.path_entry.set_text(common_prefix, set_file_chooser_folder=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(): + self.completion_popup.popdown() + +class PathChooserComboBox(gtk.HBox, StoredValuesPopup, gobject.GObject): + + __gsignals__ = { + "list-value-added": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "list-value-removed": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "list-values-reordered": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "list-values-changed": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "auto-complete-enabled-toggled": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "show-filechooser-toggled": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "show-path-entry-toggled": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "show-folder-name-on-button": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "show-hidden-files-toggled": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "accelerator-set": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + "max-rows-changed": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, )), + } + + def __init__(self, max_visible_rows=20, auto_complete=True, use_completer_popup=True): + gtk.HBox.__init__(self) + gobject.GObject.__init__(self) + 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.popup_buttonbox = self.builder.get_object("buttonbox") + self.builder.add_from_file(get_resource("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.folder_name_label = self.builder.get_object("folder_name_label") + self.default_text = None + self.button_properties = self.builder.get_object("button_properties") + self.combo_hbox = self.builder.get_object("entry_combobox_hbox") + # Change the parent of the hbox from the glade Window to this hbox. + self.combo_hbox.reparent(self) + StoredValuesPopup.__init__(self, self.builder, self, max_visible_rows, self.combo_hbox) + self.tooltips = gtk.Tooltips() + + 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, + } + 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): + """ + Set the text for the entry. + + """ + self.text_entry.set_text(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.tooltips.set_tip(self.combo_hbox, text) + if default_text: + self.default_text = text + self.tooltips.set_tip(self.button_default, "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 + if not self.path_entry_visible: + # Show entire path + self.folder_name_label.set_text(text) + else: + if self.show_folder_name_on_button: + text = path_without_trailing_path_sep(text) + if not text is "/" and os.path.basename(text): + text = os.path.basename(text) + else: + text = "" + self.folder_name_label.set_text(text) + + 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, e: + raise TypeError("TypeError when setting accelerator string: %s" % str(e)) + + def get_auto_complete_enabled(self): + return self.auto_completer.auto_complete_enabled + + def set_auto_complete_enabled(self, enable): + if not type(enable) is 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 type(show) is 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 type(enable) is 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 type(visible) is 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 type(visible) is 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 type(show) is 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 type(enable) is 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, value, paths): + """ + Perform the auto completion with the provided paths + """ + self.auto_completer._end_completion(value, paths) + +###################################### +## Callbacks and internal functions +###################################### + + def _on_entry_text_focus_out_event(self, widget, event): + 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.PACK_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.PACK_START) + else: + self.combo_hbox.set_child_packing(self.filechooser_button, 1, 1, 0, gtk.PACK_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): + dialog = self.filechooserdialog + dialog.set_current_folder(self.get_text()) + response_id = dialog.run() + + if response_id == 0: + text = self.filechooserdialog.get_filename() + self.set_text(text) + dialog.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. + + """ + keyval = event.keyval + state = event.state & gtk.accelerator_get_default_mod_mask() + ctrl = event.state & gtk.gdk.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(PathChooserComboBox, self).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) + 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") + close_button = self.builder.get_object("config_dialog_button_close") + 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.popup_window) + + 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 + self.tooltips.set_tip(set_key_button, "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 + self.tooltips.set_tip(set_key_button, 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.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.state & gtk.gdk.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 sys + w = gtk.Window() + w.set_position(gtk.WIN_POS_CENTER) + w.set_size_request(600, -1) + w.set_title('ComboEntry example') + w.connect('delete-event', gtk.main_quit) + + box1 = gtk.VBox(gtk.FALSE, 0) + + def get_resource2(filename): + return "%s/glade/%s" % (os.path.abspath(os.path.dirname(sys.argv[0])), 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) + + 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(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/gtkui/preferences.py b/deluge/ui/gtkui/preferences.py index e217f3925..b546b6fc3 100644 --- a/deluge/ui/gtkui/preferences.py +++ b/deluge/ui/gtkui/preferences.py @@ -42,6 +42,7 @@ import logging import deluge.component as component from deluge.ui.client import client +from deluge.ui.gtkui.path_chooser import PathChooser import deluge.common import common import dialogs @@ -74,6 +75,7 @@ class Preferences(component.Component): self.treeview = self.builder.get_object("treeview") self.notebook = self.builder.get_object("notebook") self.gtkui_config = ConfigManager("gtkui.conf") + self.window_open = False self.load_pref_dialog_state() @@ -190,6 +192,24 @@ class Preferences(component.Component): self.all_plugins = [] self.enabled_plugins = [] + self.setup_path_choosers() + + 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") + 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") + 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") + self.copy_torrents_to_hbox.add(self.copy_torrent_files_path_chooser) + self.copy_torrents_to_hbox.show_all() + def __del__(self): del self.gtkui_config @@ -252,6 +272,7 @@ class Preferences(component.Component): def show(self, page=None): """Page should be the string in the left list.. ie, 'Network' or 'Bandwidth'""" + self.window_open = True if page != None: for (index, string) in self.liststore: if page == string: @@ -260,7 +281,6 @@ class Preferences(component.Component): component.get("PluginManager").run_on_show_prefs() - # Update the preferences dialog to reflect current config settings self.core_config = {} if client.connected(): @@ -291,253 +311,146 @@ class Preferences(component.Component): else: self._show() - def _show(self): - if self.core_config != {} and self.core_config != None: - core_widgets = { - "download_path_button": \ - ("filename", self.core_config["download_location"]), - "chk_move_completed": \ - ("active", self.core_config["move_completed"]), - "move_completed_path_button": \ - ("filename", self.core_config["move_completed_path"]), - "chk_copy_torrent_file": \ - ("active", self.core_config["copy_torrent_file"]), - "chk_del_copy_torrent_file": \ - ("active", self.core_config["del_copy_torrent_file"]), - "torrent_files_button": \ - ("filename", self.core_config["torrentfiles_location"]), - "radio_compact_allocation": \ - ("active", self.core_config["compact_allocation"]), - "radio_full_allocation": \ - ("not_active", self.core_config["compact_allocation"]), - "chk_prioritize_first_last_pieces": \ - ("active", - self.core_config["prioritize_first_last_pieces"]), - "chk_sequential_download": \ - ("active", - self.core_config["sequential_download"]), - "chk_add_paused": ("active", self.core_config["add_paused"]), - "spin_port_min": ("value", self.core_config["listen_ports"][0]), - "spin_port_max": ("value", self.core_config["listen_ports"][1]), - "active_port_label": ("text", str(self.active_port)), - "chk_random_port": ("active", self.core_config["random_port"]), - "spin_outgoing_port_min": ("value", self.core_config["outgoing_ports"][0]), - "spin_outgoing_port_max": ("value", self.core_config["outgoing_ports"][1]), - "chk_random_outgoing_ports": ("active", self.core_config["random_outgoing_ports"]), - "entry_interface": ("text", self.core_config["listen_interface"]), - "entry_peer_tos": ("text", self.core_config["peer_tos"]), - "chk_dht": ("active", self.core_config["dht"]), - "chk_upnp": ("active", self.core_config["upnp"]), - "chk_natpmp": ("active", self.core_config["natpmp"]), - "chk_utpex": ("active", self.core_config["utpex"]), - "chk_lt_tex": ("active", self.core_config["lt_tex"]), - "chk_lsd": ("active", self.core_config["lsd"]), - "chk_new_releases": ("active", self.core_config["new_release_check"]), - "chk_send_info": ("active", self.core_config["send_info"]), - "entry_geoip": ("text", self.core_config["geoip_db_location"]), - "combo_encin": ("active", self.core_config["enc_in_policy"]), - "combo_encout": ("active", self.core_config["enc_out_policy"]), - "combo_enclevel": ("active", self.core_config["enc_level"]), - "spin_max_connections_global": \ - ("value", self.core_config["max_connections_global"]), - "spin_max_download": \ - ("value", self.core_config["max_download_speed"]), - "spin_max_upload": \ - ("value", self.core_config["max_upload_speed"]), - "spin_max_upload_slots_global": \ - ("value", self.core_config["max_upload_slots_global"]), - "spin_max_half_open_connections": \ - ("value", self.core_config["max_half_open_connections"]), - "spin_max_connections_per_second": \ - ("value", self.core_config["max_connections_per_second"]), - "chk_ignore_limits_on_local_network": \ - ("active", self.core_config["ignore_limits_on_local_network"]), - "chk_rate_limit_ip_overhead": \ - ("active", self.core_config["rate_limit_ip_overhead"]), - "spin_max_connections_per_torrent": \ - ("value", self.core_config["max_connections_per_torrent"]), - "spin_max_upload_slots_per_torrent": \ - ("value", self.core_config["max_upload_slots_per_torrent"]), - "spin_max_download_per_torrent": \ - ("value", self.core_config["max_download_speed_per_torrent"]), - "spin_max_upload_per_torrent": \ - ("value", self.core_config["max_upload_speed_per_torrent"]), - "spin_daemon_port": \ - ("value", self.core_config["daemon_port"]), - "chk_allow_remote_connections": \ - ("active", self.core_config["allow_remote"]), - "spin_active": ("value", self.core_config["max_active_limit"]), - "spin_seeding": ("value", self.core_config["max_active_seeding"]), - "spin_downloading": ("value", self.core_config["max_active_downloading"]), - "chk_dont_count_slow_torrents": ("active", self.core_config["dont_count_slow_torrents"]), - "chk_auto_manage_prefer_seeds": ("active", self.core_config["auto_manage_prefer_seeds"]), - "chk_queue_new_top": ("active", self.core_config["queue_new_to_top"]), - "spin_share_ratio_limit": ("value", self.core_config["share_ratio_limit"]), - "spin_seed_time_ratio_limit": \ - ("value", self.core_config["seed_time_ratio_limit"]), - "spin_seed_time_limit": ("value", self.core_config["seed_time_limit"]), - "chk_seed_ratio": ("active", self.core_config["stop_seed_at_ratio"]), - "spin_share_ratio": ("value", self.core_config["stop_seed_ratio"]), - "chk_remove_ratio": ("active", self.core_config["remove_seed_at_ratio"]), - "spin_cache_size": ("value", self.core_config["cache_size"]), - "spin_cache_expiry": ("value", self.core_config["cache_expiry"]) - } - # Add proxy stuff - for t in ("peer", "web_seed", "tracker", "dht"): - core_widgets["spin_proxy_port_%s" % t] = ( - "value", self.core_config["proxies"][t]["port"] - ) - core_widgets["combo_proxy_type_%s" % t] = ( - "active", self.core_config["proxies"][t]["type"] - ) - core_widgets["txt_proxy_server_%s" % t] = ( - "text", self.core_config["proxies"][t]["hostname"] - ) - core_widgets["txt_proxy_username_%s" % t] = ( - "text", self.core_config["proxies"][t]["username"] - ) - core_widgets["txt_proxy_password_%s" % t] = ( - "text", self.core_config["proxies"][t]["password"] - ) + def start(self): + if self.window_open: + self.show() - # Change a few widgets if we're connected to a remote host - if not client.is_localhost(): - self.builder.get_object("entry_download_path").show() - self.builder.get_object("download_path_button").hide() - core_widgets.pop("download_path_button") - core_widgets["entry_download_path"] = ( - "text", self.core_config["download_location"] - ) + def stop(self): + self.core_config = None + if self.window_open: + self._show() - self.builder.get_object("entry_move_completed_path").show() - self.builder.get_object("move_completed_path_button").hide() - core_widgets.pop("move_completed_path_button") - core_widgets["entry_move_completed_path"] = ( - "text", self.core_config["move_completed_path"] - ) + def _show(self): + self.is_connected = self.core_config != {} and self.core_config != 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"), + "radio_compact_allocation": ("active", "compact_allocation"), + "radio_full_allocation": ("not_active", "compact_allocation"), + "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_port_min": ("value", lambda: self.core_config["listen_ports"][0]), + "spin_port_max": ("value", lambda: self.core_config["listen_ports"][1]), + "chk_random_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_peer_tos": ("text", "peer_tos"), + "chk_dht": ("active", "dht"), + "chk_upnp": ("active", "upnp"), + "chk_natpmp": ("active", "natpmp"), + "chk_utpex": ("active", "utpex"), + "chk_lt_tex": ("active", "lt_tex"), + "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_seed_ratio": ("active", "stop_seed_at_ratio"), + "spin_share_ratio": ("value", "stop_seed_ratio"), + "chk_remove_ratio": ("active", "remove_seed_at_ratio"), + "spin_cache_size": ("value", "cache_size"), + "spin_cache_expiry": ("value", "cache_expiry"), + "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), + } + + # Add proxy stuff + for t in ("peer", "web_seed", "tracker", "dht"): + core_widgets["spin_proxy_port_%s" % t] = ( + "value", lambda: self.core_config["proxies"][t]["port"] + ) + core_widgets["combo_proxy_type_%s" % t] = ( + "active", lambda: self.core_config["proxies"][t]["type"] + ) + core_widgets["txt_proxy_server_%s" % t] = ( + "text", lambda: self.core_config["proxies"][t]["hostname"] + ) + core_widgets["txt_proxy_username_%s" % t] = ( + "text", lambda: self.core_config["proxies"][t]["username"] + ) + core_widgets["txt_proxy_password_%s" % t] = ( + "text", lambda: self.core_config["proxies"][t]["password"] + ) - self.builder.get_object("entry_torrents_path").show() - self.builder.get_object("torrent_files_button").hide() - core_widgets.pop("torrent_files_button") - core_widgets["entry_torrents_path"] = ( - "text", self.core_config["torrentfiles_location"] - ) - else: - self.builder.get_object("entry_download_path").hide() - self.builder.get_object("download_path_button").show() - self.builder.get_object("entry_move_completed_path").hide() - self.builder.get_object("move_completed_path_button").show() - self.builder.get_object("entry_torrents_path").hide() - self.builder.get_object("torrent_files_button").show() - - # Update the widgets accordingly - for key in core_widgets.keys(): - modifier = core_widgets[key][0] - value = core_widgets[key][1] + 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.keys(): + modifier = core_widgets[key][0] + if type(key) is str: widget = self.builder.get_object(key) - if type(widget) == gtk.FileChooserButton: - for child in widget.get_children(): - child.set_sensitive(True) - widget.set_sensitive(True) - - if modifier == "filename": - if value: - try: - widget.set_current_folder(value) - except Exception, e: - log.debug("Unable to set_current_folder: %s", e) - elif 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": - widget.set_text(value) + else: + widget = key + + widget.set_sensitive(self.is_connected) + if self.is_connected: + value = core_widgets[key][1] + from types import FunctionType + if type(value) is FunctionType: + value = value() + elif type(value) is str: + value = self.core_config[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": + 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.keys(): - widget = self.builder.get_object(key) + if type(key) is str: + widget = self.builder.get_object(key) + else: + widget = key # Update the toggle status if necessary self.on_toggle(widget) - else: - core_widget_list = [ - "download_path_button", - "chk_move_completed", - "move_completed_path_button", - "chk_copy_torrent_file", - "chk_del_copy_torrent_file", - "torrent_files_button", - "radio_compact_allocation", - "radio_full_allocation", - "chk_prioritize_first_last_pieces", - "chk_sequential_download", - "chk_add_paused", - "spin_port_min", - "spin_port_max", - "active_port_label", - "chk_random_port", - "spin_outgoing_port_min", - "spin_outgoing_port_max", - "chk_random_outgoing_ports", - "entry_interface", - "entry_peer_tos", - "chk_dht", - "chk_upnp", - "chk_natpmp", - "chk_utpex", - "chk_lt_tex", - "chk_lsd", - "chk_send_info", - "chk_new_releases", - "entry_geoip", - "combo_encin", - "combo_encout", - "combo_enclevel", - "spin_max_connections_global", - "spin_max_download", - "spin_max_upload", - "spin_max_upload_slots_global", - "spin_max_half_open_connections", - "spin_max_connections_per_second", - "chk_ignore_limits_on_local_network", - "chk_rate_limit_ip_overhead", - "spin_max_connections_per_torrent", - "spin_max_upload_slots_per_torrent", - "spin_max_download_per_torrent", - "spin_max_upload_per_torrent", - "spin_daemon_port", - "chk_allow_remote_connections", - "spin_seeding", - "spin_downloading", - "spin_active", - "chk_dont_count_slow_torrents", - "chk_auto_manage_prefer_seeds", - "chk_queue_new_top", - "chk_seed_ratio", - "spin_share_ratio", - "chk_remove_ratio", - "spin_share_ratio_limit", - "spin_seed_time_ratio_limit", - "spin_seed_time_limit", - "spin_cache_size", - "spin_cache_expiry", - "button_cache_refresh", - "btn_testport" - ] - for t in ("peer", "web_seed", "tracker", "dht"): - core_widget_list.append("spin_proxy_port_%s" % t) - core_widget_list.append("combo_proxy_type_%s" % t) - core_widget_list.append("txt_proxy_username_%s" % t) - core_widget_list.append("txt_proxy_password_%s" % t) - core_widget_list.append("txt_proxy_server_%s" % t) - - # We don't appear to be connected to a daemon - for key in core_widget_list: - widget = self.builder.get_object(key) - if type(widget) == gtk.FileChooserButton: - for child in widget.get_children(): - child.set_sensitive(False) - widget.set_sensitive(False) ## Downloads tab ## self.builder.get_object("chk_show_dialog").set_active( @@ -576,7 +489,6 @@ class Preferences(component.Component): 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() @@ -636,20 +548,10 @@ class Preferences(component.Component): 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() - if client.is_localhost(): - new_core_config["download_location"] = \ - self.builder.get_object("download_path_button").get_filename() - new_core_config["move_completed_path"] = \ - self.builder.get_object("move_completed_path_button").get_filename() - new_core_config["torrentfiles_location"] = \ - self.builder.get_object("torrent_files_button").get_filename() - else: - new_core_config["download_location"] = \ - self.builder.get_object("entry_download_path").get_text() - new_core_config["move_completed_path"] = \ - self.builder.get_object("entry_move_completed_path").get_text() - new_core_config["torrentfiles_location"] = \ - self.builder.get_object("entry_torrents_path").get_text() + + 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["compact_allocation"] = \ self.builder.get_object("radio_compact_allocation").get_active() @@ -872,6 +774,7 @@ class Preferences(component.Component): dialog.run() def hide(self): + self.window_open = False self.builder.get_object("port_img").hide() self.pref_dialog.hide() @@ -918,6 +821,11 @@ class Preferences(component.Component): except: 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_port": {"spin_port_min": False, @@ -932,8 +840,8 @@ class Preferences(component.Component): "password_label": True}, "radio_open_folder_custom": {"combo_file_manager": False, "txt_open_folder_location": True}, - "chk_move_completed" : {"move_completed_path_button" : True}, - "chk_copy_torrent_file" : {"torrent_files_button" : 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_seed_ratio" : {"spin_share_ratio": True, "chk_remove_ratio" : True} @@ -942,9 +850,12 @@ class Preferences(component.Component): def update_dependent_widgets(name, value): dependency = dependents[name] for dep in dependency.keys(): - depwidget = self.builder.get_object(dep) + 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) + depwidget.set_sensitive(sensitive and self.is_connected) if dep in dependents: update_dependent_widgets(dep, depwidget.get_active() and sensitive) @@ -1085,7 +996,7 @@ class Preferences(component.Component): # If incoming and outgoing both set to disabled, disable level combobox if combo_encin == 2 and combo_encout == 2: combo_enclevel.set_sensitive(False) - else: + elif self.is_connected: combo_enclevel.set_sensitive(True) def _on_combo_proxy_type_changed(self, widget): -- cgit