mirror of
https://github.com/transmission/transmission
synced 2024-12-27 18:18:10 +00:00
1129 lines
36 KiB
C++
1129 lines
36 KiB
C++
// This file Copyright © 2007-2022 Mnemosyne LLC.
|
|
// It may be used under GPLv2 (SPDX: GPL-2.0), GPLv3 (SPDX: GPL-3.0),
|
|
// or any future license endorsed by Mnemosyne LLC.
|
|
// License text can be found in the licenses/ folder.
|
|
|
|
#include <climits> /* USHRT_MAX, INT_MAX */
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <unistd.h>
|
|
|
|
#include <glibmm.h>
|
|
#include <glibmm/i18n.h>
|
|
|
|
#include <libtransmission/transmission.h>
|
|
#include <libtransmission/utils.h>
|
|
#include <libtransmission/version.h>
|
|
#include <libtransmission/web-utils.h>
|
|
|
|
#include "FreeSpaceLabel.h"
|
|
#include "HigWorkarea.h"
|
|
#include "Prefs.h"
|
|
#include "PrefsDialog.h"
|
|
#include "Session.h"
|
|
#include "Utils.h"
|
|
|
|
/**
|
|
***
|
|
**/
|
|
|
|
class PrefsDialog::Impl
|
|
{
|
|
public:
|
|
Impl(PrefsDialog& dialog, Glib::RefPtr<Session> const& core);
|
|
~Impl();
|
|
|
|
TR_DISABLE_COPY_MOVE(Impl)
|
|
|
|
private:
|
|
Gtk::Widget* speedPage();
|
|
Gtk::Widget* downloadingPage();
|
|
Gtk::Widget* seedingPage();
|
|
Gtk::Widget* privacyPage();
|
|
Gtk::Widget* networkPage();
|
|
Gtk::Widget* desktopPage();
|
|
Gtk::Widget* remotePage();
|
|
|
|
void on_core_prefs_changed(tr_quark const key);
|
|
|
|
void response_cb(int response);
|
|
|
|
private:
|
|
PrefsDialog& dialog_;
|
|
|
|
Glib::RefPtr<Session> const core_;
|
|
sigc::connection core_prefs_tag_;
|
|
|
|
FreeSpaceLabel* freespace_label_ = nullptr;
|
|
|
|
#if 0
|
|
Gtk::Label* port_label_ = nullptr;
|
|
Gtk::Button* port_button_ = nullptr;
|
|
Gtk::SpinButton* port_spin_ = nullptr;
|
|
#endif
|
|
};
|
|
|
|
/**
|
|
***
|
|
**/
|
|
|
|
void PrefsDialog::Impl::response_cb(int response)
|
|
{
|
|
if (response == Gtk::RESPONSE_HELP)
|
|
{
|
|
gtr_open_uri(gtr_get_help_uri() + "/html/preferences.html");
|
|
}
|
|
|
|
if (response == Gtk::RESPONSE_CLOSE)
|
|
{
|
|
dialog_.hide();
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
Gtk::CheckButton* new_check_button(Glib::ustring const& mnemonic, tr_quark const key, Glib::RefPtr<Session> const& core)
|
|
{
|
|
auto* w = Gtk::make_managed<Gtk::CheckButton>(mnemonic, true);
|
|
w->set_active(gtr_pref_flag_get(key));
|
|
w->signal_toggled().connect([w, key, core]() { core->set_pref(key, w->get_active()); });
|
|
return w;
|
|
}
|
|
|
|
auto const IdleDataKey = Glib::Quark("idle-data");
|
|
|
|
bool spun_cb_idle(Gtk::SpinButton* spin, tr_quark const key, Glib::RefPtr<Session> const& core, bool isDouble)
|
|
{
|
|
bool keep_waiting = true;
|
|
auto* last_change = static_cast<Glib::Timer*>(spin->get_data(IdleDataKey));
|
|
|
|
/* has the user stopped making changes? */
|
|
if (last_change->elapsed() > 0.33)
|
|
{
|
|
/* update the core */
|
|
if (isDouble)
|
|
{
|
|
double const value = spin->get_value();
|
|
core->set_pref(key, value);
|
|
}
|
|
else
|
|
{
|
|
int const value = spin->get_value_as_int();
|
|
core->set_pref(key, value);
|
|
}
|
|
|
|
/* cleanup */
|
|
spin->set_data(IdleDataKey, nullptr);
|
|
keep_waiting = false;
|
|
spin->unreference();
|
|
}
|
|
|
|
return keep_waiting;
|
|
}
|
|
|
|
void spun_cb(Gtk::SpinButton* w, tr_quark const key, Glib::RefPtr<Session> const& core, bool isDouble)
|
|
{
|
|
/* user may be spinning through many values, so let's hold off
|
|
for a moment to keep from flooding the core with changes */
|
|
auto* last_change = static_cast<Glib::Timer*>(w->get_data(IdleDataKey));
|
|
|
|
if (last_change == nullptr)
|
|
{
|
|
last_change = new Glib::Timer();
|
|
w->set_data(IdleDataKey, last_change, [](gpointer p) { delete static_cast<Glib::Timer*>(p); });
|
|
w->reference();
|
|
Glib::signal_timeout().connect_seconds([w, key, core, isDouble]() { return spun_cb_idle(w, key, core, isDouble); }, 1);
|
|
}
|
|
|
|
last_change->start();
|
|
}
|
|
|
|
Gtk::SpinButton* new_spin_button(tr_quark const key, Glib::RefPtr<Session> const& core, int low, int high, int step)
|
|
{
|
|
auto* w = Gtk::make_managed<Gtk::SpinButton>(Gtk::Adjustment::create(gtr_pref_int_get(key), low, high, step));
|
|
w->set_digits(0);
|
|
w->signal_value_changed().connect([w, key, core]() { spun_cb(w, key, core, false); });
|
|
return w;
|
|
}
|
|
|
|
Gtk::SpinButton* new_spin_button_double(
|
|
tr_quark const key,
|
|
Glib::RefPtr<Session> const& core,
|
|
double low,
|
|
double high,
|
|
double step)
|
|
{
|
|
auto* w = Gtk::make_managed<Gtk::SpinButton>(Gtk::Adjustment::create(gtr_pref_double_get(key), low, high, step));
|
|
w->set_digits(2);
|
|
w->signal_value_changed().connect([w, key, core]() { spun_cb(w, key, core, true); });
|
|
return w;
|
|
}
|
|
|
|
void entry_changed_cb(Gtk::Entry* w, tr_quark const key, Glib::RefPtr<Session> const& core)
|
|
{
|
|
core->set_pref(key, w->get_text());
|
|
}
|
|
|
|
Gtk::Entry* new_entry(tr_quark const key, Glib::RefPtr<Session> const& core)
|
|
{
|
|
auto* w = Gtk::make_managed<Gtk::Entry>();
|
|
|
|
if (auto const value = gtr_pref_string_get(key); !value.empty())
|
|
{
|
|
w->set_text(value);
|
|
}
|
|
|
|
w->signal_changed().connect([w, key, core]() { entry_changed_cb(w, key, core); });
|
|
return w;
|
|
}
|
|
|
|
void chosen_cb(Gtk::FileChooser* w, tr_quark const key, Glib::RefPtr<Session> const& core)
|
|
{
|
|
core->set_pref(key, w->get_filename());
|
|
}
|
|
|
|
Gtk::FileChooserButton* new_path_chooser_button(tr_quark const key, Glib::RefPtr<Session> const& core)
|
|
{
|
|
auto* w = Gtk::make_managed<Gtk::FileChooserButton>(Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER);
|
|
|
|
if (auto const path = gtr_pref_string_get(key); !path.empty())
|
|
{
|
|
w->set_filename(path);
|
|
}
|
|
|
|
w->signal_selection_changed().connect([w, key, core]() { chosen_cb(w, key, core); });
|
|
return w;
|
|
}
|
|
|
|
Gtk::FileChooserButton* new_file_chooser_button(tr_quark const key, Glib::RefPtr<Session> const& core)
|
|
{
|
|
auto* w = Gtk::make_managed<Gtk::FileChooserButton>(Gtk::FILE_CHOOSER_ACTION_OPEN);
|
|
|
|
if (auto const path = gtr_pref_string_get(key); !path.empty())
|
|
{
|
|
w->set_filename(path);
|
|
}
|
|
|
|
w->signal_selection_changed().connect([w, key, core]() { chosen_cb(w, key, core); });
|
|
return w;
|
|
}
|
|
|
|
void target_cb(Gtk::ToggleButton* tb, Gtk::Widget* target)
|
|
{
|
|
target->set_sensitive(tb->get_active());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
/****
|
|
***** Download Tab
|
|
****/
|
|
|
|
Gtk::Widget* PrefsDialog::Impl::downloadingPage()
|
|
{
|
|
guint row = 0;
|
|
|
|
auto* t = Gtk::make_managed<HigWorkarea>();
|
|
t->add_section_title(row, C_("Gerund", "Adding"));
|
|
|
|
{
|
|
auto* l = new_check_button(_("Automatically add .torrent files _from:"), TR_KEY_watch_dir_enabled, core_);
|
|
auto* w = new_path_chooser_button(TR_KEY_watch_dir, core_);
|
|
w->set_sensitive(gtr_pref_flag_get(TR_KEY_watch_dir_enabled));
|
|
l->signal_toggled().connect([l, w]() { target_cb(l, w); });
|
|
t->add_row_w(row, *l, *w);
|
|
}
|
|
|
|
t->add_wide_control(row, *new_check_button(_("Show the Torrent Options _dialog"), TR_KEY_show_options_window, core_));
|
|
|
|
t->add_wide_control(row, *new_check_button(_("_Start added torrents"), TR_KEY_start_added_torrents, core_));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("Mo_ve .torrent file to the trash"), TR_KEY_trash_original_torrent_files, core_));
|
|
|
|
t->add_row(row, _("Save to _Location:"), *new_path_chooser_button(TR_KEY_download_dir, core_));
|
|
|
|
freespace_label_ = Gtk::make_managed<FreeSpaceLabel>(core_);
|
|
freespace_label_->set_halign(Gtk::ALIGN_END);
|
|
freespace_label_->set_valign(Gtk::ALIGN_CENTER);
|
|
t->add_wide_control(row, *freespace_label_);
|
|
|
|
t->add_section_divider(row);
|
|
t->add_section_title(row, _("Download Queue"));
|
|
|
|
t->add_row(row, _("Ma_ximum active downloads:"), *new_spin_button(TR_KEY_download_queue_size, core_, 0, INT_MAX, 1));
|
|
|
|
t->add_row(
|
|
row,
|
|
_("Downloads sharing data in the last _N minutes are active:"),
|
|
*new_spin_button(TR_KEY_queue_stalled_minutes, core_, 1, INT_MAX, 15));
|
|
|
|
t->add_section_divider(row);
|
|
t->add_section_title(row, _("Incomplete"));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("Append \"._part\" to incomplete files' names"), TR_KEY_rename_partial_files, core_));
|
|
|
|
{
|
|
auto* l = new_check_button(_("Keep _incomplete torrents in:"), TR_KEY_incomplete_dir_enabled, core_);
|
|
auto* w = new_path_chooser_button(TR_KEY_incomplete_dir, core_);
|
|
w->set_sensitive(gtr_pref_flag_get(TR_KEY_incomplete_dir_enabled));
|
|
l->signal_toggled().connect([l, w]() { target_cb(l, w); });
|
|
t->add_row_w(row, *l, *w);
|
|
}
|
|
|
|
{
|
|
auto* l = new_check_button(_("Call scrip_t when torrent is completed:"), TR_KEY_script_torrent_done_enabled, core_);
|
|
auto* w = new_file_chooser_button(TR_KEY_script_torrent_done_filename, core_);
|
|
w->set_sensitive(gtr_pref_flag_get(TR_KEY_script_torrent_done_enabled));
|
|
l->signal_toggled().connect([l, w]() { target_cb(l, w); });
|
|
t->add_row_w(row, *l, *w);
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Torrent Tab
|
|
****/
|
|
|
|
Gtk::Widget* PrefsDialog::Impl::seedingPage()
|
|
{
|
|
guint row = 0;
|
|
|
|
auto* t = Gtk::make_managed<HigWorkarea>();
|
|
t->add_section_title(row, _("Limits"));
|
|
|
|
{
|
|
auto* w = new_check_button(_("Stop seeding at _ratio:"), TR_KEY_ratio_limit_enabled, core_);
|
|
auto* w2 = new_spin_button_double(TR_KEY_ratio_limit, core_, 0, 1000, .05);
|
|
w2->set_sensitive(gtr_pref_flag_get(TR_KEY_ratio_limit_enabled));
|
|
w->signal_toggled().connect([w, w2]() { target_cb(w, w2); });
|
|
t->add_row_w(row, *w, *w2);
|
|
}
|
|
|
|
{
|
|
auto* w = new_check_button(_("Stop seeding if idle for _N minutes:"), TR_KEY_idle_seeding_limit_enabled, core_);
|
|
auto* w2 = new_spin_button(TR_KEY_idle_seeding_limit, core_, 1, 40320, 5);
|
|
w2->set_sensitive(gtr_pref_flag_get(TR_KEY_idle_seeding_limit_enabled));
|
|
w->signal_toggled().connect([w, w2]() { target_cb(w, w2); });
|
|
t->add_row_w(row, *w, *w2);
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Desktop Tab
|
|
****/
|
|
|
|
Gtk::Widget* PrefsDialog::Impl::desktopPage()
|
|
{
|
|
guint row = 0;
|
|
|
|
auto* t = Gtk::make_managed<HigWorkarea>();
|
|
t->add_section_title(row, _("Desktop"));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("_Inhibit hibernation when torrents are active"), TR_KEY_inhibit_desktop_hibernation, core_));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("Show Transmission icon in the _notification area"), TR_KEY_show_notification_area_icon, core_));
|
|
|
|
t->add_section_divider(row);
|
|
t->add_section_title(row, _("Notification"));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("Show a notification when torrents are a_dded"), TR_KEY_torrent_added_notification_enabled, core_));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("Show a notification when torrents _finish"), TR_KEY_torrent_complete_notification_enabled, core_));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("Play a _sound when torrents finish"), TR_KEY_torrent_complete_sound_enabled, core_));
|
|
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Peer Tab
|
|
****/
|
|
|
|
namespace
|
|
{
|
|
|
|
struct blocklist_data
|
|
{
|
|
blocklist_data() = default;
|
|
~blocklist_data();
|
|
|
|
TR_DISABLE_COPY_MOVE(blocklist_data)
|
|
|
|
sigc::connection updateBlocklistTag;
|
|
Gtk::Button* updateBlocklistButton = nullptr;
|
|
std::unique_ptr<Gtk::MessageDialog> updateBlocklistDialog;
|
|
Gtk::Label* label = nullptr;
|
|
Gtk::CheckButton* check = nullptr;
|
|
Glib::RefPtr<Session> core;
|
|
};
|
|
|
|
void updateBlocklistText(Gtk::Label* w, Glib::RefPtr<Session> const& core)
|
|
{
|
|
int const n = tr_blocklistGetRuleCount(core->get_session());
|
|
w->set_markup(
|
|
gtr_sprintf("<i>%s</i>", gtr_sprintf(ngettext("Blocklist contains %'d rule", "Blocklist contains %'d rules", n), n)));
|
|
}
|
|
|
|
/* prefs dialog is being destroyed, so stop listening to blocklist updates */
|
|
blocklist_data::~blocklist_data()
|
|
{
|
|
if (updateBlocklistTag.connected())
|
|
{
|
|
updateBlocklistTag.disconnect();
|
|
}
|
|
}
|
|
|
|
/* user hit "close" in the blocklist-update dialog */
|
|
void onBlocklistUpdateResponse(std::shared_ptr<blocklist_data> const& data)
|
|
{
|
|
data->updateBlocklistButton->set_sensitive(true);
|
|
data->updateBlocklistDialog.reset();
|
|
data->updateBlocklistTag.disconnect();
|
|
}
|
|
|
|
/* core says the blocklist was updated */
|
|
void onBlocklistUpdated(Glib::RefPtr<Session> const& core, int n, blocklist_data* data)
|
|
{
|
|
bool const success = n >= 0;
|
|
int const count = n >= 0 ? n : tr_blocklistGetRuleCount(core->get_session());
|
|
data->updateBlocklistButton->set_sensitive(true);
|
|
data->updateBlocklistDialog->set_message(success ? _("<b>Update succeeded!</b>") : _("<b>Unable to update.</b>"), true);
|
|
data->updateBlocklistDialog->set_secondary_text(
|
|
gtr_sprintf(ngettext("Blocklist has %'d rule.", "Blocklist has %'d rules.", count), count));
|
|
updateBlocklistText(data->label, core);
|
|
}
|
|
|
|
/* user pushed a button to update the blocklist */
|
|
void onBlocklistUpdate(Gtk::Button* w, std::shared_ptr<blocklist_data> const& data)
|
|
{
|
|
data->updateBlocklistDialog = std::make_unique<Gtk::MessageDialog>(
|
|
*static_cast<Gtk::Window*>(w->get_toplevel()),
|
|
_("Update Blocklist"),
|
|
false,
|
|
Gtk::MESSAGE_INFO,
|
|
Gtk::BUTTONS_CLOSE);
|
|
data->updateBlocklistButton->set_sensitive(false);
|
|
data->updateBlocklistDialog->set_secondary_text(_("Getting new blocklist…"));
|
|
data->updateBlocklistDialog->signal_response().connect([data](int /*response*/) { onBlocklistUpdateResponse(data); });
|
|
data->updateBlocklistDialog->show();
|
|
data->core->blocklist_update();
|
|
data->updateBlocklistTag = data->core->signal_blocklist_updated().connect(
|
|
[data](auto n) { onBlocklistUpdated(data->core, n, data.get()); });
|
|
}
|
|
|
|
void on_blocklist_url_changed(Gtk::Editable* e, Gtk::Button* button)
|
|
{
|
|
auto const url = e->get_chars(0, -1);
|
|
button->set_sensitive(tr_urlIsValid(url.c_str()));
|
|
}
|
|
|
|
void onIntComboChanged(Gtk::ComboBox* combo_box, tr_quark const key, Glib::RefPtr<Session> const& core)
|
|
{
|
|
core->set_pref(key, gtr_combo_box_get_active_enum(*combo_box));
|
|
}
|
|
|
|
Gtk::ComboBox* new_encryption_combo(Glib::RefPtr<Session> const& core, tr_quark const key)
|
|
{
|
|
auto* w = gtr_combo_box_new_enum({
|
|
{ _("Allow encryption"), TR_CLEAR_PREFERRED },
|
|
{ _("Prefer encryption"), TR_ENCRYPTION_PREFERRED },
|
|
{ _("Require encryption"), TR_ENCRYPTION_REQUIRED },
|
|
});
|
|
gtr_combo_box_set_active_enum(*w, gtr_pref_int_get(key));
|
|
w->signal_changed().connect([w, key, core]() { onIntComboChanged(w, key, core); });
|
|
return w;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Gtk::Widget* PrefsDialog::Impl::privacyPage()
|
|
{
|
|
guint row = 0;
|
|
|
|
auto const data = std::make_shared<blocklist_data>();
|
|
data->core = core_;
|
|
|
|
auto* t = Gtk::make_managed<HigWorkarea>();
|
|
t->add_section_title(row, _("Privacy"));
|
|
|
|
t->add_row(row, _("_Encryption mode:"), *new_encryption_combo(core_, TR_KEY_encryption));
|
|
|
|
t->add_section_divider(row);
|
|
t->add_section_title(row, _("Blocklist"));
|
|
|
|
data->check = new_check_button(_("Enable _blocklist:"), TR_KEY_blocklist_enabled, core_);
|
|
auto* e = new_entry(TR_KEY_blocklist_url, core_);
|
|
e->set_size_request(300, -1);
|
|
t->add_row_w(row, *data->check, *e);
|
|
data->check->signal_toggled().connect([data, e]() { target_cb(data->check, e); });
|
|
target_cb(data->check, e);
|
|
|
|
data->label = Gtk::make_managed<Gtk::Label>();
|
|
data->label->set_halign(Gtk::ALIGN_START);
|
|
data->label->set_valign(Gtk::ALIGN_CENTER);
|
|
updateBlocklistText(data->label, core_);
|
|
auto* h = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, GUI_PAD_BIG);
|
|
h->pack_start(*data->label, true, true, 0);
|
|
data->updateBlocklistButton = Gtk::make_managed<Gtk::Button>(_("_Update"), true);
|
|
data->updateBlocklistButton->set_data("session", core_->get_session());
|
|
data->updateBlocklistButton->signal_clicked().connect([data]() { onBlocklistUpdate(data->updateBlocklistButton, data); });
|
|
target_cb(data->check, data->updateBlocklistButton);
|
|
h->pack_start(*data->updateBlocklistButton, false, false, 0);
|
|
data->check->signal_toggled().connect([data]() { target_cb(data->check, data->label); });
|
|
target_cb(data->check, data->label);
|
|
t->add_wide_control(row, *h);
|
|
e->signal_changed().connect([data, e]() { on_blocklist_url_changed(e, data->updateBlocklistButton); });
|
|
on_blocklist_url_changed(e, data->updateBlocklistButton);
|
|
|
|
auto* update_check = new_check_button(_("Enable _automatic updates"), TR_KEY_blocklist_updates_enabled, core_);
|
|
t->add_wide_control(row, *update_check);
|
|
data->check->signal_toggled().connect([data, update_check]() { target_cb(data->check, update_check); });
|
|
target_cb(data->check, update_check);
|
|
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Remote Tab
|
|
****/
|
|
|
|
namespace
|
|
{
|
|
|
|
class WhitelistModelColumns : public Gtk::TreeModelColumnRecord
|
|
{
|
|
public:
|
|
WhitelistModelColumns()
|
|
{
|
|
add(address);
|
|
}
|
|
|
|
Gtk::TreeModelColumn<Glib::ustring> address;
|
|
};
|
|
|
|
WhitelistModelColumns const whitelist_cols;
|
|
|
|
Glib::RefPtr<Gtk::ListStore> whitelist_tree_model_new(std::string const& whitelist)
|
|
{
|
|
auto const store = Gtk::ListStore::create(whitelist_cols);
|
|
|
|
std::istringstream stream(whitelist);
|
|
std::string s;
|
|
|
|
while (std::getline(stream, s, ','))
|
|
{
|
|
s = gtr_str_strip(s);
|
|
|
|
if (s.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto const iter = store->append();
|
|
(*iter)[whitelist_cols.address] = s;
|
|
}
|
|
|
|
return store;
|
|
}
|
|
|
|
struct remote_page
|
|
{
|
|
Glib::RefPtr<Session> core;
|
|
Gtk::TreeView* view;
|
|
Glib::RefPtr<Gtk::ListStore> store;
|
|
Gtk::Button* remove_button;
|
|
std::vector<Gtk::Widget*> widgets;
|
|
std::vector<Gtk::Widget*> auth_widgets;
|
|
std::vector<Gtk::Widget*> whitelist_widgets;
|
|
Gtk::ToggleButton* rpc_tb;
|
|
Gtk::ToggleButton* auth_tb;
|
|
Gtk::ToggleButton* whitelist_tb;
|
|
};
|
|
|
|
void refreshWhitelist(std::shared_ptr<remote_page> const& page)
|
|
{
|
|
std::ostringstream gstr;
|
|
|
|
for (auto const& row : page->store->children())
|
|
{
|
|
gstr << row.get_value(whitelist_cols.address) << ",";
|
|
}
|
|
|
|
auto str = gstr.str();
|
|
if (!str.empty())
|
|
{
|
|
str.resize(str.size() - 1); /* remove the trailing comma */
|
|
}
|
|
|
|
page->core->set_pref(TR_KEY_rpc_whitelist, str);
|
|
}
|
|
|
|
void onAddressEdited(Glib::ustring const& path, Glib::ustring const& address, std::shared_ptr<remote_page> const& page)
|
|
{
|
|
if (auto const iter = page->store->get_iter(path); iter)
|
|
{
|
|
(*iter)[whitelist_cols.address] = address;
|
|
}
|
|
|
|
refreshWhitelist(page);
|
|
}
|
|
|
|
void onAddWhitelistClicked(std::shared_ptr<remote_page> const& page)
|
|
{
|
|
auto const iter = page->store->append();
|
|
(*iter)[whitelist_cols.address] = "0.0.0.0";
|
|
|
|
page->view->set_cursor(page->store->get_path(iter), *page->view->get_column(0), true);
|
|
}
|
|
|
|
void onRemoveWhitelistClicked(std::shared_ptr<remote_page> const& page)
|
|
{
|
|
auto const sel = page->view->get_selection();
|
|
|
|
if (auto const iter = sel->get_selected(); iter)
|
|
{
|
|
page->store->erase(iter);
|
|
refreshWhitelist(page);
|
|
}
|
|
}
|
|
|
|
void refreshRPCSensitivity(std::shared_ptr<remote_page> const& page)
|
|
{
|
|
bool const rpc_active = page->rpc_tb->get_active();
|
|
bool const auth_active = page->auth_tb->get_active();
|
|
bool const whitelist_active = page->whitelist_tb->get_active();
|
|
auto const sel = page->view->get_selection();
|
|
auto const have_addr = sel->get_selected();
|
|
auto const n_rules = page->store->children().size();
|
|
|
|
for (auto* const widget : page->widgets)
|
|
{
|
|
widget->set_sensitive(rpc_active);
|
|
}
|
|
|
|
for (auto* const widget : page->auth_widgets)
|
|
{
|
|
widget->set_sensitive(rpc_active && auth_active);
|
|
}
|
|
|
|
for (auto* const widget : page->whitelist_widgets)
|
|
{
|
|
widget->set_sensitive(rpc_active && whitelist_active);
|
|
}
|
|
|
|
page->remove_button->set_sensitive(rpc_active && whitelist_active && have_addr && n_rules > 1);
|
|
}
|
|
|
|
void onLaunchClutchCB()
|
|
{
|
|
gtr_open_uri(gtr_sprintf("http://localhost:%d/", (int)gtr_pref_int_get(TR_KEY_rpc_port)));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Gtk::Widget* PrefsDialog::Impl::remotePage()
|
|
{
|
|
guint row = 0;
|
|
auto const page = std::make_shared<remote_page>();
|
|
|
|
page->core = core_;
|
|
|
|
auto* t = Gtk::make_managed<HigWorkarea>();
|
|
|
|
t->add_section_title(row, _("Remote Control"));
|
|
|
|
/* "enabled" checkbutton */
|
|
page->rpc_tb = new_check_button(_("Allow _remote access"), TR_KEY_rpc_enabled, core_);
|
|
page->rpc_tb->signal_clicked().connect([page]() { refreshRPCSensitivity(page); });
|
|
auto* h1 = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, GUI_PAD_BIG);
|
|
h1->pack_start(*page->rpc_tb, true, true, 0);
|
|
auto* open_button = Gtk::make_managed<Gtk::Button>(_("_Open web client"), true);
|
|
page->widgets.push_back(open_button);
|
|
open_button->signal_clicked().connect(&onLaunchClutchCB);
|
|
h1->pack_start(*open_button, false, false, 0);
|
|
t->add_wide_control(row, *h1);
|
|
|
|
/* port */
|
|
auto* port_spin = new_spin_button(TR_KEY_rpc_port, core_, 0, USHRT_MAX, 1);
|
|
page->widgets.push_back(port_spin);
|
|
page->widgets.push_back(t->add_row(row, _("HTTP _port:"), *port_spin));
|
|
|
|
/* require authentication */
|
|
page->auth_tb = new_check_button(_("Use _authentication"), TR_KEY_rpc_authentication_required, core_);
|
|
t->add_wide_control(row, *page->auth_tb);
|
|
page->widgets.push_back(page->auth_tb);
|
|
page->auth_tb->signal_clicked().connect([page]() { refreshRPCSensitivity(page); });
|
|
|
|
/* username */
|
|
auto* username_entry = new_entry(TR_KEY_rpc_username, core_);
|
|
page->auth_widgets.push_back(username_entry);
|
|
page->auth_widgets.push_back(t->add_row(row, _("_Username:"), *username_entry));
|
|
|
|
/* password */
|
|
auto* password_entry = new_entry(TR_KEY_rpc_password, core_);
|
|
password_entry->set_visibility(false);
|
|
page->auth_widgets.push_back(password_entry);
|
|
page->auth_widgets.push_back(t->add_row(row, _("Pass_word:"), *password_entry));
|
|
|
|
/* require authentication */
|
|
page->whitelist_tb = new_check_button(_("Only allow these IP a_ddresses:"), TR_KEY_rpc_whitelist_enabled, core_);
|
|
t->add_wide_control(row, *page->whitelist_tb);
|
|
page->widgets.push_back(page->whitelist_tb);
|
|
page->whitelist_tb->signal_clicked().connect([page]() { refreshRPCSensitivity(page); });
|
|
|
|
/* access control list */
|
|
{
|
|
page->store = whitelist_tree_model_new(gtr_pref_string_get(TR_KEY_rpc_whitelist));
|
|
|
|
page->view = Gtk::make_managed<Gtk::TreeView>(page->store);
|
|
page->view->signal_button_release_event().connect([page](GdkEventButton* event)
|
|
{ return on_tree_view_button_released(page->view, event); });
|
|
|
|
page->whitelist_widgets.push_back(page->view);
|
|
page->view->set_tooltip_text(_("IP addresses may use wildcards, such as 192.168.*.*"));
|
|
auto const sel = page->view->get_selection();
|
|
sel->signal_changed().connect([page]() { refreshRPCSensitivity(page); });
|
|
page->view->set_headers_visible(true);
|
|
auto* view_frame = Gtk::make_managed<Gtk::Frame>();
|
|
view_frame->set_shadow_type(Gtk::SHADOW_IN);
|
|
view_frame->add(*page->view);
|
|
|
|
/* ip address column */
|
|
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
|
|
r->signal_edited().connect([page](auto const& path, auto const& new_text) { onAddressEdited(path, new_text, page); });
|
|
r->property_editable() = true;
|
|
auto* c = Gtk::make_managed<Gtk::TreeViewColumn>("", *r);
|
|
c->add_attribute(r->property_text(), whitelist_cols.address);
|
|
c->set_expand(true);
|
|
page->view->append_column(*c);
|
|
page->view->set_headers_visible(false);
|
|
|
|
auto* w = t->add_row(row, _("Addresses:"), *view_frame);
|
|
w->set_halign(Gtk::ALIGN_START);
|
|
w->set_valign(Gtk::ALIGN_START);
|
|
w->set_margin_top(GUI_PAD);
|
|
w->set_margin_bottom(GUI_PAD);
|
|
page->whitelist_widgets.push_back(w);
|
|
|
|
auto* h2 = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, GUI_PAD);
|
|
page->remove_button = Gtk::make_managed<Gtk::Button>(_("_Remove"), true);
|
|
page->remove_button->signal_clicked().connect([page]() { onRemoveWhitelistClicked(page); });
|
|
refreshRPCSensitivity(page);
|
|
h2->pack_start(*page->remove_button, true, true, 0);
|
|
auto* add_button = Gtk::make_managed<Gtk::Button>(_("_Add"), true);
|
|
page->whitelist_widgets.push_back(add_button);
|
|
add_button->signal_clicked().connect([page]() { onAddWhitelistClicked(page); });
|
|
h2->set_halign(Gtk::ALIGN_END);
|
|
h2->set_valign(Gtk::ALIGN_CENTER);
|
|
h2->pack_start(*add_button, true, true, 0);
|
|
t->add_wide_control(row, *h2);
|
|
}
|
|
|
|
refreshRPCSensitivity(page);
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Bandwidth Tab
|
|
****/
|
|
|
|
namespace
|
|
{
|
|
|
|
struct BandwidthPage
|
|
{
|
|
std::vector<Gtk::Widget*> sched_widgets;
|
|
};
|
|
|
|
void refreshSchedSensitivity(std::shared_ptr<BandwidthPage> const& p)
|
|
{
|
|
bool const sched_enabled = gtr_pref_flag_get(TR_KEY_alt_speed_time_enabled);
|
|
|
|
for (auto* const w : p->sched_widgets)
|
|
{
|
|
w->set_sensitive(sched_enabled);
|
|
}
|
|
}
|
|
|
|
Gtk::ComboBox* new_time_combo(Glib::RefPtr<Session> const& core, tr_quark const key)
|
|
{
|
|
class TimeModelColumns : public Gtk::TreeModelColumnRecord
|
|
{
|
|
public:
|
|
TimeModelColumns()
|
|
{
|
|
add(offset);
|
|
add(title);
|
|
}
|
|
|
|
Gtk::TreeModelColumn<int> offset;
|
|
Gtk::TreeModelColumn<Glib::ustring> title;
|
|
};
|
|
|
|
static TimeModelColumns const time_cols;
|
|
|
|
/* build a store at 15 minute intervals */
|
|
auto store = Gtk::ListStore::create(time_cols);
|
|
|
|
for (int i = 0; i < 60 * 24; i += 15)
|
|
{
|
|
auto const iter = store->append();
|
|
(*iter)[time_cols.offset] = i;
|
|
(*iter)[time_cols.title] = gtr_sprintf("%02d:%02d", i / 60, i % 60);
|
|
}
|
|
|
|
/* build the widget */
|
|
auto* w = Gtk::make_managed<Gtk::ComboBox>(static_cast<Glib::RefPtr<Gtk::TreeModel> const&>(store));
|
|
w->set_wrap_width(4);
|
|
auto* r = Gtk::make_managed<Gtk::CellRendererText>();
|
|
w->pack_start(*r, true);
|
|
w->add_attribute(r->property_text(), time_cols.title);
|
|
w->set_active(gtr_pref_int_get(key) / 15);
|
|
w->signal_changed().connect(
|
|
[w, key, core]()
|
|
{
|
|
if (auto const iter = w->get_active(); iter)
|
|
{
|
|
core->set_pref(key, iter->get_value(time_cols.offset));
|
|
}
|
|
});
|
|
|
|
return w;
|
|
}
|
|
|
|
Gtk::ComboBox* new_week_combo(Glib::RefPtr<Session> const& core, tr_quark const key)
|
|
{
|
|
auto* w = gtr_combo_box_new_enum({
|
|
{ _("Every Day"), TR_SCHED_ALL },
|
|
{ _("Weekdays"), TR_SCHED_WEEKDAY },
|
|
{ _("Weekends"), TR_SCHED_WEEKEND },
|
|
{ _("Sunday"), TR_SCHED_SUN },
|
|
{ _("Monday"), TR_SCHED_MON },
|
|
{ _("Tuesday"), TR_SCHED_TUES },
|
|
{ _("Wednesday"), TR_SCHED_WED },
|
|
{ _("Thursday"), TR_SCHED_THURS },
|
|
{ _("Friday"), TR_SCHED_FRI },
|
|
{ _("Saturday"), TR_SCHED_SAT },
|
|
});
|
|
gtr_combo_box_set_active_enum(*w, gtr_pref_int_get(key));
|
|
w->signal_changed().connect([w, key, core]() { onIntComboChanged(w, key, core); });
|
|
return w;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Gtk::Widget* PrefsDialog::Impl::speedPage()
|
|
{
|
|
guint row = 0;
|
|
auto page = std::make_shared<BandwidthPage>();
|
|
|
|
auto* t = Gtk::make_managed<HigWorkarea>();
|
|
t->add_section_title(row, _("Speed Limits"));
|
|
|
|
{
|
|
auto* w = new_check_button(gtr_sprintf(_("_Upload (%s):"), _(speed_K_str)), TR_KEY_speed_limit_up_enabled, core_);
|
|
auto* w2 = new_spin_button(TR_KEY_speed_limit_up, core_, 0, INT_MAX, 5);
|
|
w2->set_sensitive(gtr_pref_flag_get(TR_KEY_speed_limit_up_enabled));
|
|
w->signal_toggled().connect([w, w2]() { target_cb(w, w2); });
|
|
t->add_row_w(row, *w, *w2);
|
|
}
|
|
|
|
{
|
|
auto* w = new_check_button(gtr_sprintf(_("_Download (%s):"), _(speed_K_str)), TR_KEY_speed_limit_down_enabled, core_);
|
|
auto* w2 = new_spin_button(TR_KEY_speed_limit_down, core_, 0, INT_MAX, 5);
|
|
w2->set_sensitive(gtr_pref_flag_get(TR_KEY_speed_limit_down_enabled));
|
|
w->signal_toggled().connect([w, w2]() { target_cb(w, w2); });
|
|
t->add_row_w(row, *w, *w2);
|
|
}
|
|
|
|
t->add_section_divider(row);
|
|
|
|
{
|
|
auto* h = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, GUI_PAD);
|
|
auto* w = Gtk::make_managed<Gtk::Label>(gtr_sprintf("<b>%s</b>", _("Alternative Speed Limits")));
|
|
w->set_halign(Gtk::ALIGN_START);
|
|
w->set_valign(Gtk::ALIGN_CENTER);
|
|
w->set_use_markup(true);
|
|
h->pack_start(*w, false, false, 0);
|
|
h->pack_start(*Gtk::make_managed<Gtk::Image>("alt-speed-on", Gtk::ICON_SIZE_MENU), false, false, 0);
|
|
t->add_section_title_widget(row, *h);
|
|
}
|
|
|
|
{
|
|
auto* w = Gtk::make_managed<Gtk::Label>(
|
|
gtr_sprintf("<small>%s</small>", _("Override normal speed limits manually or at scheduled times")));
|
|
w->set_use_markup(true);
|
|
w->set_halign(Gtk::ALIGN_START);
|
|
w->set_valign(Gtk::ALIGN_CENTER);
|
|
t->add_wide_control(row, *w);
|
|
}
|
|
|
|
t->add_row(
|
|
row,
|
|
gtr_sprintf(_("U_pload (%s):"), _(speed_K_str)),
|
|
*new_spin_button(TR_KEY_alt_speed_up, core_, 0, INT_MAX, 5));
|
|
|
|
t->add_row(
|
|
row,
|
|
gtr_sprintf(_("Do_wnload (%s):"), _(speed_K_str)),
|
|
*new_spin_button(TR_KEY_alt_speed_down, core_, 0, INT_MAX, 5));
|
|
|
|
{
|
|
auto* h = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 0);
|
|
auto* start_combo = new_time_combo(core_, TR_KEY_alt_speed_time_begin);
|
|
page->sched_widgets.push_back(start_combo);
|
|
h->pack_start(*start_combo, true, true, 0);
|
|
auto* to_label = Gtk::make_managed<Gtk::Label>(_(" _to "), true);
|
|
page->sched_widgets.push_back(to_label);
|
|
h->pack_start(*to_label, false, false, 0);
|
|
auto* end_combo = new_time_combo(core_, TR_KEY_alt_speed_time_end);
|
|
to_label->set_mnemonic_widget(*end_combo);
|
|
page->sched_widgets.push_back(end_combo);
|
|
h->pack_start(*end_combo, true, true, 0);
|
|
auto* w = new_check_button(_("_Scheduled times:"), TR_KEY_alt_speed_time_enabled, core_);
|
|
w->signal_toggled().connect([page]() { refreshSchedSensitivity(page); });
|
|
t->add_row_w(row, *w, *h);
|
|
}
|
|
|
|
auto* week_combo = new_week_combo(core_, TR_KEY_alt_speed_time_day);
|
|
page->sched_widgets.push_back(week_combo);
|
|
page->sched_widgets.push_back(t->add_row(row, _("_On days:"), *week_combo));
|
|
|
|
refreshSchedSensitivity(page);
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
***** Network Tab
|
|
****/
|
|
|
|
namespace
|
|
{
|
|
|
|
struct network_page_data
|
|
{
|
|
network_page_data() = default;
|
|
~network_page_data();
|
|
|
|
TR_DISABLE_COPY_MOVE(network_page_data)
|
|
|
|
Glib::RefPtr<Session> core;
|
|
Gtk::Label* portLabel = nullptr;
|
|
Gtk::Button* portButton = nullptr;
|
|
Gtk::SpinButton* portSpin = nullptr;
|
|
sigc::connection portTag;
|
|
sigc::connection prefsTag;
|
|
};
|
|
|
|
void onCorePrefsChanged(tr_quark const key, network_page_data* data)
|
|
{
|
|
if (key == TR_KEY_peer_port)
|
|
{
|
|
gtr_label_set_text(*data->portLabel, _("Status unknown"));
|
|
data->portButton->set_sensitive(true);
|
|
data->portSpin->set_sensitive(true);
|
|
}
|
|
}
|
|
|
|
network_page_data::~network_page_data()
|
|
{
|
|
if (prefsTag.connected())
|
|
{
|
|
prefsTag.disconnect();
|
|
}
|
|
|
|
if (portTag.connected())
|
|
{
|
|
portTag.disconnect();
|
|
}
|
|
}
|
|
|
|
void onPortTested(bool isOpen, network_page_data* data)
|
|
{
|
|
data->portLabel->set_markup(isOpen ? _("Port is <b>open</b>") : _("Port is <b>closed</b>"));
|
|
data->portButton->set_sensitive(true);
|
|
data->portSpin->set_sensitive(true);
|
|
}
|
|
|
|
void onPortTest(std::shared_ptr<network_page_data> const& data)
|
|
{
|
|
data->portButton->set_sensitive(false);
|
|
data->portSpin->set_sensitive(false);
|
|
data->portLabel->set_markup(_("<i>Testing TCP port…</i>"));
|
|
|
|
if (!data->portTag.connected())
|
|
{
|
|
data->portTag = data->core->signal_port_tested().connect([data](bool is_open) { onPortTested(is_open, data.get()); });
|
|
}
|
|
|
|
data->core->port_test();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Gtk::Widget* PrefsDialog::Impl::networkPage()
|
|
{
|
|
guint row = 0;
|
|
|
|
/* register to stop listening to core prefs changes when the page is destroyed */
|
|
auto const data = std::make_shared<network_page_data>();
|
|
data->core = core_;
|
|
|
|
/* build the page */
|
|
auto* t = Gtk::make_managed<HigWorkarea>();
|
|
t->add_section_title(row, _("Listening Port"));
|
|
|
|
data->portSpin = new_spin_button(TR_KEY_peer_port, core_, 1, USHRT_MAX, 1);
|
|
t->add_row(row, _("_Port used for incoming connections:"), *data->portSpin);
|
|
|
|
auto* h = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, GUI_PAD_BIG);
|
|
data->portLabel = Gtk::make_managed<Gtk::Label>(_("Status unknown"));
|
|
data->portLabel->set_halign(Gtk::ALIGN_START);
|
|
data->portLabel->set_valign(Gtk::ALIGN_CENTER);
|
|
h->pack_start(*data->portLabel, true, true, 0);
|
|
data->portButton = Gtk::make_managed<Gtk::Button>(_("Te_st Port"), true);
|
|
h->pack_end(*data->portButton, false, false, 0);
|
|
data->portButton->signal_clicked().connect([data]() { onPortTest(data); });
|
|
t->add_row(row, {}, *h);
|
|
data->prefsTag = core_->signal_prefs_changed().connect([data](auto key) { onCorePrefsChanged(key, data.get()); });
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(
|
|
_("Pick a _random port every time Transmission is started"),
|
|
TR_KEY_peer_port_random_on_start,
|
|
core_));
|
|
|
|
t->add_wide_control(
|
|
row,
|
|
*new_check_button(_("Use UPnP or NAT-PMP port _forwarding from my router"), TR_KEY_port_forwarding_enabled, core_));
|
|
|
|
t->add_section_divider(row);
|
|
t->add_section_title(row, _("Peer Limits"));
|
|
|
|
t->add_row(row, _("Maximum peers per _torrent:"), *new_spin_button(TR_KEY_peer_limit_per_torrent, core_, 1, FD_SETSIZE, 5));
|
|
t->add_row(row, _("Maximum peers _overall:"), *new_spin_button(TR_KEY_peer_limit_global, core_, 1, FD_SETSIZE, 5));
|
|
|
|
t->add_section_divider(row);
|
|
t->add_section_title(row, _("Options"));
|
|
|
|
Gtk::CheckButton* w;
|
|
|
|
#ifdef WITH_UTP
|
|
w = new_check_button(_("Enable _uTP for peer communication"), TR_KEY_utp_enabled, core_);
|
|
w->set_tooltip_text(_("uTP is a tool for reducing network congestion."));
|
|
t->add_wide_control(row, *w);
|
|
#endif
|
|
|
|
w = new_check_button(_("Use PE_X to find more peers"), TR_KEY_pex_enabled, core_);
|
|
w->set_tooltip_text(_("PEX is a tool for exchanging peer lists with the peers you're connected to."));
|
|
t->add_wide_control(row, *w);
|
|
|
|
w = new_check_button(_("Use _DHT to find more peers"), TR_KEY_dht_enabled, core_);
|
|
w->set_tooltip_text(_("DHT is a tool for finding peers without a tracker."));
|
|
t->add_wide_control(row, *w);
|
|
|
|
w = new_check_button(_("Use _Local Peer Discovery to find more peers"), TR_KEY_lpd_enabled, core_);
|
|
w->set_tooltip_text(_("LPD is a tool for finding peers on your local network."));
|
|
t->add_wide_control(row, *w);
|
|
|
|
return t;
|
|
}
|
|
|
|
/****
|
|
*****
|
|
****/
|
|
|
|
PrefsDialog::Impl::~Impl()
|
|
{
|
|
if (core_prefs_tag_.connected())
|
|
{
|
|
core_prefs_tag_.disconnect();
|
|
}
|
|
}
|
|
|
|
void PrefsDialog::Impl::on_core_prefs_changed(tr_quark const key)
|
|
{
|
|
#if 0
|
|
|
|
if (key == TR_KEY_peer_port)
|
|
{
|
|
port_label_->set_text(_("Status unknown"));
|
|
port_button_->set_sensitive(true);
|
|
port_spin_->set_sensitive(true);
|
|
}
|
|
|
|
#endif
|
|
|
|
if (key == TR_KEY_download_dir)
|
|
{
|
|
char const* downloadDir = tr_sessionGetDownloadDir(core_->get_session());
|
|
freespace_label_->set_dir(downloadDir);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<PrefsDialog> PrefsDialog::create(Gtk::Window& parent, Glib::RefPtr<Session> const& core)
|
|
{
|
|
return std::unique_ptr<PrefsDialog>(new PrefsDialog(parent, core));
|
|
}
|
|
|
|
PrefsDialog::PrefsDialog(Gtk::Window& parent, Glib::RefPtr<Session> const& core)
|
|
: Gtk::Dialog(_("Transmission Preferences"), parent)
|
|
, impl_(std::make_unique<Impl>(*this, core))
|
|
{
|
|
set_modal(true);
|
|
}
|
|
|
|
PrefsDialog::~PrefsDialog() = default;
|
|
|
|
PrefsDialog::Impl::Impl(PrefsDialog& dialog, Glib::RefPtr<Session> const& core)
|
|
: dialog_(dialog)
|
|
, core_(core)
|
|
{
|
|
static tr_quark const prefs_quarks[] = { TR_KEY_peer_port, TR_KEY_download_dir };
|
|
|
|
core_prefs_tag_ = core_->signal_prefs_changed().connect(sigc::mem_fun(*this, &Impl::on_core_prefs_changed));
|
|
|
|
dialog_.add_button(_("_Help"), Gtk::RESPONSE_HELP);
|
|
dialog_.add_button(_("_Close"), Gtk::RESPONSE_CLOSE);
|
|
dialog_.set_role("transmission-preferences-dialog");
|
|
dialog_.set_border_width(GUI_PAD);
|
|
|
|
auto* n = Gtk::make_managed<Gtk::Notebook>();
|
|
n->set_border_width(GUI_PAD);
|
|
|
|
n->append_page(*speedPage(), _("Speed"));
|
|
n->append_page(*downloadingPage(), C_("Gerund", "Downloading"));
|
|
n->append_page(*seedingPage(), C_("Gerund", "Seeding"));
|
|
n->append_page(*privacyPage(), _("Privacy"));
|
|
n->append_page(*networkPage(), _("Network"));
|
|
n->append_page(*desktopPage(), _("Desktop"));
|
|
n->append_page(*remotePage(), _("Remote"));
|
|
|
|
/* init from prefs keys */
|
|
for (auto const key : prefs_quarks)
|
|
{
|
|
on_core_prefs_changed(key);
|
|
}
|
|
|
|
dialog_.signal_response().connect(sigc::mem_fun(*this, &Impl::response_cb));
|
|
gtr_dialog_set_content(dialog_, *n);
|
|
}
|