diff --git a/CMakeLists.txt b/CMakeLists.txt index fc87796..dfbefe5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,11 +22,11 @@ endif () find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network Svg LinguistTools) if (NKR_CROSS) - set_property(TARGET Qt5::moc PROPERTY IMPORTED_LOCATION /usr/bin/moc) - set_property(TARGET Qt5::uic PROPERTY IMPORTED_LOCATION /usr/bin/uic) - set_property(TARGET Qt5::rcc PROPERTY IMPORTED_LOCATION /usr/bin/rcc) - set_property(TARGET Qt5::lrelease PROPERTY IMPORTED_LOCATION /usr/bin/lrelease) - set_property(TARGET Qt5::lupdate PROPERTY IMPORTED_LOCATION /usr/bin/lupdate) + set_property(TARGET Qt6::moc PROPERTY IMPORTED_LOCATION /usr/bin/moc) + set_property(TARGET Qt6::uic PROPERTY IMPORTED_LOCATION /usr/bin/uic) + set_property(TARGET Qt6::rcc PROPERTY IMPORTED_LOCATION /usr/bin/rcc) + set_property(TARGET Qt6::lrelease PROPERTY IMPORTED_LOCATION /usr/bin/lrelease) + set_property(TARGET Qt6::lupdate PROPERTY IMPORTED_LOCATION /usr/bin/lupdate) endif () #### Platform Variables #### @@ -250,10 +250,15 @@ set(PROJECT_SOURCES ui/widget/GroupItem.cpp ui/widget/GroupItem.h ui/widget/GroupItem.ui + ui/widget/RouteItem.cpp + ui/widget/RouteItem.h + ui/widget/RouteItem.ui res/neko.qrc res/theme/feiyangqingyun/qss.qrc ${QV2RAY_RC} + db/RouteEntity.h + db/RouteEntity.cpp ) # Qt exe diff --git a/db/ConfigBuilder.cpp b/db/ConfigBuilder.cpp index 1414fb9..9b023d2 100644 --- a/db/ConfigBuilder.cpp +++ b/db/ConfigBuilder.cpp @@ -493,11 +493,6 @@ namespace NekoGui { status->result->coreConfig.insert("inbounds", status->inbounds); status->result->coreConfig.insert("outbounds", status->outbounds); - // user rule - if (!status->forTest) { - DOMAIN_USER_RULE - IP_USER_RULE - } // sing-box common rule object auto make_rule = [&](const QStringList &list, bool isIP = false) { @@ -722,12 +717,7 @@ namespace NekoGui { if (geosite.isEmpty()) status->result->error = +"geosite.db not found"; // final add routing rule - auto routingRules = QString2QJsonObject(dataStore->routing->custom)["rules"].toArray(); - if (status->forTest) routingRules = {}; - if (!status->forTest) QJSONARRAY_ADD(routingRules, QString2QJsonObject(dataStore->custom_route_global)["rules"].toArray()) - QJSONARRAY_ADD(routingRules, status->routingRules) auto routeObj = QJsonObject{ - {"rules", routingRules}, {"auto_detect_interface", true}, { "geoip", diff --git a/db/Database.cpp b/db/Database.cpp index 29950e5..24a1ade 100644 --- a/db/Database.cpp +++ b/db/Database.cpp @@ -37,8 +37,10 @@ namespace NekoGui { // profiles = {}; groups = {}; + routes = {}; profilesIdOrder = filterIntJsonFile("profiles"); groupsIdOrder = filterIntJsonFile("groups"); + routesIdOrder = filterIntJsonFile("routes"); // Load Proxys QList delProfile; for (auto id: profilesIdOrder) { @@ -75,12 +77,29 @@ namespace NekoGui { groupsTabOrder << id; } } + // Load Routing profiles + for (auto id : routesIdOrder) { + auto route = LoadRouteChain(QString("routes/%1.json").arg(id)); + if (route == nullptr) { + MW_show_log(QString("File routes/%1.json is corrupted, consider manually handling it").arg(id)); + continue; + } + + routes[id] = route; + } + // First setup if (groups.empty()) { auto defaultGroup = NekoGui::ProfileManager::NewGroup(); defaultGroup->name = QObject::tr("Default"); NekoGui::profileManager->AddGroup(defaultGroup); } + + if (routes.empty()) { + auto defaultRoute = NekoGui::RoutingChain::GetDefaultChain(); + NekoGui::profileManager->AddRouteChain(defaultRoute); + } + // if (dataStore->flag_reorder) { { @@ -163,6 +182,16 @@ namespace NekoGui { return ent; } + std::shared_ptr ProfileManager::LoadRouteChain(const QString &jsonPath) { + std::shared_ptr routingChain; + routingChain->fn = jsonPath; + if (!routingChain->Load()) { + return nullptr; + } + + return routingChain; + } + // 新建的不给 fn 和 id std::shared_ptr ProfileManager::NewProxyEntity(const QString &type) { @@ -373,6 +402,37 @@ namespace NekoGui { return GetGroup(dataStore->current_group); } + + std::shared_ptr ProfileManager::NewRouteChain() { + auto route = std::make_shared(); + return route; + } + + int ProfileManager::NewRouteChainID() const { + if (routes.empty()) { + return 0; + } + return routesIdOrder.last() + 1; + } + + bool ProfileManager::AddRouteChain(const std::shared_ptr chain) { + if (chain->id >= 0) { + return false; + } + + chain->id = NewRouteChainID(); + routes[chain->id] = chain; + routesIdOrder.push_back(chain->id); + chain->fn = QString("routes/%1.json").arg(chain->id); + chain->Save(); + + return true; + } + + std::shared_ptr ProfileManager::GetRouteChain(int id) { + return routes.count(id) > 0 ? routes[id] : nullptr; + } + QList> Group::Profiles() const { QList> ret; for (const auto &[_, profile]: profileManager->profiles) { diff --git a/db/Database.hpp b/db/Database.hpp index dc7272f..4e4e87f 100644 --- a/db/Database.hpp +++ b/db/Database.hpp @@ -3,6 +3,7 @@ #include "main/NekoGui.hpp" #include "ProxyEntity.hpp" #include "Group.hpp" +#include "RouteEntity.h" namespace NekoGui { class ProfileManager : private JsonStore { @@ -16,6 +17,7 @@ namespace NekoGui { std::map> profiles; std::map> groups; + std::map> routes; ProfileManager(); @@ -28,6 +30,8 @@ namespace NekoGui { [[nodiscard]] static std::shared_ptr NewGroup(); + [[nodiscard]] static std::shared_ptr NewRouteChain(); + bool AddProfile(const std::shared_ptr &ent, int gid = -1); void DeleteProfile(int id); @@ -44,18 +48,27 @@ namespace NekoGui { std::shared_ptr CurrentGroup(); + bool AddRouteChain(std::shared_ptr chain); + + std::shared_ptr GetRouteChain(int id); + private: // sort by id QList profilesIdOrder; QList groupsIdOrder; + QList routesIdOrder; [[nodiscard]] int NewProfileID() const; [[nodiscard]] int NewGroupID() const; + [[nodiscard]] int NewRouteChainID() const; + static std::shared_ptr LoadProxyEntity(const QString &jsonPath); static std::shared_ptr LoadGroup(const QString &jsonPath); + + static std::shared_ptr LoadRouteChain(const QString &jsonPath); }; extern ProfileManager *profileManager; diff --git a/db/RouteEntity.cpp b/db/RouteEntity.cpp new file mode 100644 index 0000000..063f9be --- /dev/null +++ b/db/RouteEntity.cpp @@ -0,0 +1,252 @@ +#include +#include +#include "RouteEntity.h" +#include "db/Database.hpp" + +namespace NekoGui { + QJsonArray get_as_array(const QList& str, bool castToNum = false) { + QJsonArray res; + for (const auto &item: str) { + if (castToNum) res.append(item.toInt()); + else res.append(item); + } + return res; + } + + QJsonObject RouteRule::get_rule_json() const { + QJsonObject obj; + + if (ip_version != "") obj["ip_version"] = ip_version.toInt(); + if (network != "") obj["network"] = network; + if (protocol != "") obj["protocol"] = protocol; + if (!domain.empty()) obj["domain"] = get_as_array(domain); + if (!domain_suffix.empty()) obj["domain_suffix"] = get_as_array(domain_suffix); + if (!domain_keyword.empty()) obj["domain_keyword"] = get_as_array(domain_keyword); + if (!domain_regex.empty()) obj["domain_regex"] = get_as_array(domain_regex); + if (!source_ip_cidr.empty()) obj["source_ip_cidr"] = get_as_array(source_ip_cidr); + if (source_ip_is_private != nullptr) obj["source_ip_is_private"] = *source_ip_is_private; + if (!ip_cidr.empty()) obj["ip_cidr"] = get_as_array(ip_cidr); + if (ip_is_private != nullptr) obj["ip_is_private"] = *ip_is_private; + if (!source_port.empty()) obj["source_port"] = get_as_array(source_port, true); + if (!source_port_range.empty()) obj["source_port_range"] = get_as_array(source_port_range); + if (!port.empty()) obj["port"] = get_as_array(port, true); + if (!port_range.empty()) obj["port_range"] = get_as_array(port_range); + if (!process_name.empty()) obj["process_name"] = get_as_array(process_name); + if (!process_path.empty()) obj["process_path"] = get_as_array(process_path); + if (!rule_set.empty()) obj["rule_set"] = get_as_array(rule_set); + if (invert) obj["invert"] = invert; + + switch (outboundID) { // TODO use constants + case -2: + obj["outbound"] = "direct"; + case -3: + obj["outbound"] = "block"; + case -4: + obj["outbound"] = "dns_out"; + default: + auto prof = NekoGui::profileManager->GetProfile(outboundID); + if (prof == nullptr) { + MW_show_log("The outbound described in the rule chain is missing, maybe your data is corrupted"); + return {}; + } + obj["outbound"] = prof->bean->DisplayName(); + } + + return obj; + } + + + // TODO use constant for field names + QStringList RouteRule::get_attributes() { + return { + "ip_version", + "network", + "protocol", + "domain", + "domain_suffix", + "domain_keyword", + "domain_regex", + "source_ip_cidr", + "source_ip_is_private", + "ip_cidr", + "ip_is_private", + "source_port", + "source_port_range", + "port", + "port_range", + "process_name", + "process_path", + "rule_set", + "invert", + }; + } + + inputType RouteRule::get_input_type(const QString& fieldName) { + if (fieldName == "invert" || + fieldName == "source_ip_is_private" || + fieldName == "ip_is_private") return trufalse; + + if (fieldName == "ip_version" || + fieldName == "network" || + fieldName == "protocol") return select; + + return text; + } + + QStringList RouteRule::get_values_for_field(const QString& fieldName) { + if (fieldName == "ip_version") { + return {"4", "6"}; + } + if (fieldName == "network") { + return {"tcp", "udp"}; + } + if (fieldName == "protocol") { + return {"http", "tls", "quic", "stun", "dns", "bittorrent"}; + } + return {}; + } + + QStringList RouteRule::get_current_value_string(const QString& fieldName) { + if (fieldName == "ip_version" && ip_version != "") { + return {ip_version}; + } + if (fieldName == "network" && network != "") { + return {network}; + } + if (fieldName == "protocol" && protocol != "") { + return {protocol}; + } + if (fieldName == "domain") return domain; + if (fieldName == "domain_suffix") return domain_suffix; + if (fieldName == "domain_keyword") return domain_keyword; + if (fieldName == "domain_regex") return domain_regex; + if (fieldName == "source_ip_cidr") return source_ip_cidr; + if (fieldName == "ip_cidr") return ip_cidr; + if (fieldName == "source_port") return source_port; + if (fieldName == "source_port_range") return source_port_range; + if (fieldName == "port") return port; + if (fieldName == "port_range") return port_range; + if (fieldName == "process_name") return process_name; + if (fieldName == "process_path") return process_path; + if (fieldName == "rule_set") return rule_set; + return {}; + } + + bool* RouteRule::get_current_value_bool(const QString& fieldName) const { + if (fieldName == "source_ip_is_private") { + return source_ip_is_private; + } + if (fieldName == "ip_is_private") { + return ip_is_private; + } + if (fieldName == "invert") { + return reinterpret_cast(invert); + } + return nullptr; + } + + void RouteRule::set_field_value(const QString& fieldName, const QStringList& value) { + /* + * "ip_version", +"network", +"protocol", +"domain", +"domain_suffix", +"domain_keyword", +"domain_regex", +"source_ip_cidr", +"source_ip_is_private", +"ip_cidr", +"ip_is_private", +"source_port", +"source_port_range", +"port", +"port_range", +"process_name", +"process_path", +"rule_set", +"invert", + */ + if (fieldName == "ip_version") { + ip_version = value[0]; + } + if (fieldName == "network") { + network = value[0]; + } + if (fieldName == "protocol") { + protocol = value[0]; + } + if (fieldName == "domain") { + domain = value; + } + if (fieldName == "domain_suffix") { + domain_suffix = value; + } + if (fieldName == "domain_keyword") { + domain_keyword = value; + } + if (fieldName == "domain_regex") { + domain_regex = value; + } + if (fieldName == "source_ip_cidr") { + source_ip_cidr = value; + } + if (fieldName == "source_ip_is_private") { + source_ip_is_private = reinterpret_cast((value[0] == "true")); + } + if (fieldName == "ip_cidr") { + ip_cidr = value; + } + if (fieldName == "ip_is_private") { + ip_is_private = reinterpret_cast((value[0] == "true")); + } + if (fieldName == "source_port") { + source_port = value; + } + if (fieldName == "source_port_range") { + source_port_range = value; + } + if (fieldName == "port") { + port = value; + } + if (fieldName == "port_range") { + port_range = value; + } + if (fieldName == "process_name") { + process_name = value; + } + if (fieldName == "process_path") { + process_path = value; + } + if (fieldName == "rule_set") { + rule_set = value; + } + if (fieldName == "invert") { + invert = value[0]=="true"; + } + } + + QJsonArray RoutingChain::get_route_rules() { + QJsonArray res; + for (const auto &item: Rules) { + auto rule_json = item->get_rule_json(); + if (rule_json.empty()) { + MW_show_log("Aborted generating routing section, an error has occurred"); + return {}; + } + res += rule_json; + } + + return res; + } + + std::shared_ptr RoutingChain::GetDefaultChain() { + auto defaultChain = RoutingChain(); + defaultChain.name = "Default"; + auto defaultRule = RouteRule(); + defaultRule.protocol = {"dns"}; + defaultRule.outboundID = -4; + defaultChain.Rules << std::make_shared(defaultRule); + return std::make_shared(defaultChain); + } +} \ No newline at end of file diff --git a/db/RouteRuleEntity.h b/db/RouteEntity.h similarity index 54% rename from db/RouteRuleEntity.h rename to db/RouteEntity.h index 4693a14..cc0c975 100644 --- a/db/RouteRuleEntity.h +++ b/db/RouteEntity.h @@ -3,11 +3,14 @@ #include "main/NekoGui.hpp" namespace NekoGui { + enum inputType {trufalse, select, text}; + class RouteRule : public JsonStore { public: - int ip_version = 0; - QList network; - QList protocol; + QString name = ""; + QString ip_version; + QString network; + QString protocol; QList domain; QList domain_suffix; QList domain_keyword; @@ -24,11 +27,15 @@ namespace NekoGui { QList process_path; QList rule_set; bool invert = false; - int outboundID = -1; + int outboundID = -1; // -2 is direct -3 is block -4 is dns_out - QList check_for_errors(); - - QJsonObject get_rule_json(); + [[nodiscard]] QJsonObject get_rule_json() const; + static QStringList get_attributes(); + static inputType get_input_type(const QString& fieldName); + static QStringList get_values_for_field(const QString& fieldName); + QStringList get_current_value_string(const QString& fieldName); + [[nodiscard]] bool* get_current_value_bool(const QString& fieldName) const; + void set_field_value(const QString& fieldName, const QStringList& value); }; class RoutingChain : public JsonStore { @@ -39,6 +46,6 @@ namespace NekoGui { QJsonArray get_route_rules(); - QJsonArray get_default_route_rules(); + static std::shared_ptr GetDefaultChain(); }; } // namespace NekoGui \ No newline at end of file diff --git a/main/NekoGui.cpp b/main/NekoGui.cpp index e198aee..b661300 100644 --- a/main/NekoGui.cpp +++ b/main/NekoGui.cpp @@ -327,32 +327,9 @@ namespace NekoGui { // preset routing Routing::Routing(int preset) : JsonStore() { - if (preset == 1) { - direct_ip = - "geoip:cn\n" - "geoip:private"; - direct_domain = "geosite:cn"; - proxy_ip = ""; - proxy_domain = ""; - block_ip = ""; - block_domain = - "geosite:category-ads-all\n" - "domain:appcenter.ms\n" - "domain:firebase.io\n" - "domain:crashlytics.com\n"; - } - if (IS_NEKO_BOX) { - if (!Preset::SingBox::DomainStrategy.contains(domain_strategy)) domain_strategy = ""; - if (!Preset::SingBox::DomainStrategy.contains(outbound_domain_strategy)) outbound_domain_strategy = ""; - } - _add(new configItem("direct_ip", &this->direct_ip, itemType::string)); - _add(new configItem("direct_domain", &this->direct_domain, itemType::string)); - _add(new configItem("proxy_ip", &this->proxy_ip, itemType::string)); - _add(new configItem("proxy_domain", &this->proxy_domain, itemType::string)); - _add(new configItem("block_ip", &this->block_ip, itemType::string)); - _add(new configItem("block_domain", &this->block_domain, itemType::string)); - _add(new configItem("def_outbound", &this->def_outbound, itemType::string)); - _add(new configItem("custom", &this->custom, itemType::string)); + if (!Preset::SingBox::DomainStrategy.contains(domain_strategy)) domain_strategy = ""; + if (!Preset::SingBox::DomainStrategy.contains(outbound_domain_strategy)) outbound_domain_strategy = ""; + _add(new configItem("current_route_id", &this->current_route_id, itemType::integer)); // _add(new configItem("remote_dns", &this->remote_dns, itemType::string)); _add(new configItem("remote_dns_strategy", &this->remote_dns_strategy, itemType::string)); @@ -367,18 +344,6 @@ namespace NekoGui { _add(new configItem("dns_final_out", &this->dns_final_out, itemType::string)); } - QString Routing::DisplayRouting() const { - return QString("[Proxy] %1\n[Proxy] %2\n[Direct] %3\n[Direct] %4\n[Block] %5\n[Block] %6\n[Default Outbound] %7\n[DNS] %8") - .arg(SplitLinesSkipSharp(proxy_domain).join(","), 10) - .arg(SplitLinesSkipSharp(proxy_ip).join(","), 10) - .arg(SplitLinesSkipSharp(direct_domain).join(","), 10) - .arg(SplitLinesSkipSharp(direct_ip).join(","), 10) - .arg(SplitLinesSkipSharp(block_domain).join(","), 10) - .arg(SplitLinesSkipSharp(block_ip).join(","), 10) - .arg(def_outbound) - .arg(use_dns_object ? "DNS Object" : "Simple DNS"); - } - QStringList Routing::List() { QDir dr(ROUTES_PREFIX); return dr.entryList(QDir::Files); diff --git a/main/NekoGui_DataStore.hpp b/main/NekoGui_DataStore.hpp index e910298..9065f2e 100644 --- a/main/NekoGui_DataStore.hpp +++ b/main/NekoGui_DataStore.hpp @@ -4,14 +4,8 @@ namespace NekoGui { class Routing : public JsonStore { public: - QString direct_ip; - QString direct_domain; - QString proxy_ip; - QString proxy_domain; - QString block_ip; - QString block_domain; + int current_route_id = 0; QString def_outbound = "proxy"; - QString custom = "{\"rules\": []}"; // DNS QString remote_dns = "https://8.8.8.8/dns-query"; @@ -30,8 +24,6 @@ namespace NekoGui { explicit Routing(int preset = 0); - [[nodiscard]] QString DisplayRouting() const; - static QStringList List(); static bool SetToActive(const QString &name); diff --git a/ui/dialog_manage_routes.cpp b/ui/dialog_manage_routes.cpp index f35cc2e..f00e8b3 100644 --- a/ui/dialog_manage_routes.cpp +++ b/ui/dialog_manage_routes.cpp @@ -1,5 +1,7 @@ #include "dialog_manage_routes.h" #include "ui_dialog_manage_routes.h" +#include "db/Database.hpp" +//#include "ui_RouteItem.h" #include "3rdparty/qv2ray/v2/ui/widgets/editors/w_JsonEditor.hpp" #include "3rdparty/qv2ray/v3/components/GeositeReader/GeositeReader.hpp" @@ -8,38 +10,63 @@ #include #include -#include -#include -#define REFRESH_ACTIVE_ROUTING(name, obj) \ - this->active_routing = name; \ - setWindowTitle(title_base + " [" + name + "]"); \ - UpdateDisplayRouting(obj, false); +QList getRouteProfiles() { + auto routeProfiles = NekoGui::profileManager->routes; + QList res; + + for (const auto &item: routeProfiles) { + res << item.second->name; + } + return res; +} + +int getRouteID(const QString& name) { + auto routeProfiles = NekoGui::profileManager->routes; + + for (const auto &item: routeProfiles) { + if (item.second->name == name) return item.first; + } + + return -1; +} + +QString getRouteName(int id) { + return NekoGui::profileManager->routes.count(id) ? NekoGui::profileManager->routes[id]->name : ""; +} + +QList deleteItemFromList(const QList& base, const QString& target) { + QList res; + for (const auto &item: base) { + if (item == target) continue; + res << item; + } + return res; +} + +void DialogManageRoutes::reloadProfileItems() { + ui->route_prof->clear(); + ui->route_profiles->clear(); + ui->route_prof->addItems(currentRouteProfiles); + ui->route_profiles->addItems(currentRouteProfiles); +} DialogManageRoutes::DialogManageRoutes(QWidget *parent) : QDialog(parent), ui(new Ui::DialogManageRoutes) { ui->setupUi(this); - title_base = windowTitle(); + currentRouteProfiles = getRouteProfiles(); QStringList qsValue = {""}; QString dnsHelpDocumentUrl; - if (IS_NEKO_BOX) { - ui->outbound_domain_strategy->addItems(Preset::SingBox::DomainStrategy); - ui->domainStrategyCombo->addItems(Preset::SingBox::DomainStrategy); - qsValue += QString("prefer_ipv4 prefer_ipv6 ipv4_only ipv6_only").split(" "); - ui->dns_object->setPlaceholderText(DecodeB64IfValid("ewogICJzZXJ2ZXJzIjogW10sCiAgInJ1bGVzIjogW10sCiAgImZpbmFsIjogIiIsCiAgInN0cmF0ZWd5IjogIiIsCiAgImRpc2FibGVfY2FjaGUiOiBmYWxzZSwKICAiZGlzYWJsZV9leHBpcmUiOiBmYWxzZSwKICAiaW5kZXBlbmRlbnRfY2FjaGUiOiBmYWxzZSwKICAicmV2ZXJzZV9tYXBwaW5nIjogZmFsc2UsCiAgImZha2VpcCI6IHt9Cn0=")); - dnsHelpDocumentUrl = "https://sing-box.sagernet.org/configuration/dns/"; - } else { - ui->outbound_domain_strategy->addItems({"AsIs", "UseIPv4", "UseIPv6", "PreferIPv4", "PreferIPv6"}); - ui->domainStrategyCombo->addItems({"AsIs", "IPIfNonMatch", "IPOnDemand"}); - qsValue += QString("use_ip use_ip4 use_ip6").split(" "); - ui->dns_object->setPlaceholderText(DecodeB64IfValid("ewogICJzZXJ2ZXJzIjogW10KfQ==")); - dnsHelpDocumentUrl = "https://www.v2fly.org/config/dns.html"; - } + + ui->outbound_domain_strategy->addItems(Preset::SingBox::DomainStrategy); + ui->domainStrategyCombo->addItems(Preset::SingBox::DomainStrategy); + qsValue += QString("prefer_ipv4 prefer_ipv6 ipv4_only ipv6_only").split(" "); + ui->dns_object->setPlaceholderText(DecodeB64IfValid("ewogICJzZXJ2ZXJzIjogW10sCiAgInJ1bGVzIjogW10sCiAgImZpbmFsIjogIiIsCiAgInN0cmF0ZWd5IjogIiIsCiAgImRpc2FibGVfY2FjaGUiOiBmYWxzZSwKICAiZGlzYWJsZV9leHBpcmUiOiBmYWxzZSwKICAiaW5kZXBlbmRlbnRfY2FjaGUiOiBmYWxzZSwKICAicmV2ZXJzZV9tYXBwaW5nIjogZmFsc2UsCiAgImZha2VpcCI6IHt9Cn0=")); + dnsHelpDocumentUrl = "https://sing-box.sagernet.org/configuration/dns/"; + ui->direct_dns_strategy->addItems(qsValue); ui->remote_dns_strategy->addItems(qsValue); // - D_C_LOAD_STRING(custom_route_global) - // connect(ui->use_dns_object, &QCheckBox::stateChanged, this, [=](int state) { auto useDNSObject = state == Qt::Checked; ui->simple_dns_box->setDisabled(useDNSObject); @@ -57,40 +84,28 @@ DialogManageRoutes::DialogManageRoutes(QWidget *parent) : QDialog(parent), ui(ne ui->dns_object->setPlainText(QJsonObject2QString(obj, false)); } }); - // - connect(ui->custom_route_edit, &QPushButton::clicked, this, [=] { - C_EDIT_JSON_ALLOW_EMPTY(custom_route) - }); - connect(ui->custom_route_global_edit, &QPushButton::clicked, this, [=] { - C_EDIT_JSON_ALLOW_EMPTY(custom_route_global) - }); - // - builtInSchemesMenu = new QMenu(this); - builtInSchemesMenu->addActions(this->getBuiltInSchemes()); - ui->preset->setMenu(builtInSchemesMenu); + ui->sniffing_mode->setCurrentIndex(NekoGui::dataStore->routing->sniffing_mode); + ui->outbound_domain_strategy->setCurrentText(NekoGui::dataStore->routing->outbound_domain_strategy); + ui->domainStrategyCombo->setCurrentText(NekoGui::dataStore->routing->domain_strategy); + ui->use_dns_object->setChecked(NekoGui::dataStore->routing->use_dns_object); + ui->dns_object->setPlainText(NekoGui::dataStore->routing->dns_object); + ui->dns_routing->setChecked(NekoGui::dataStore->routing->dns_routing); + ui->remote_dns->setCurrentText(NekoGui::dataStore->routing->remote_dns); + ui->remote_dns_strategy->setCurrentText(NekoGui::dataStore->routing->remote_dns_strategy); + ui->direct_dns->setCurrentText(NekoGui::dataStore->routing->direct_dns); + ui->direct_dns_strategy->setCurrentText(NekoGui::dataStore->routing->direct_dns_strategy); + ui->dns_final_out->setCurrentText(NekoGui::dataStore->routing->dns_final_out); + reloadProfileItems(); + ui->route_prof->setCurrentText(getRouteName(NekoGui::dataStore->routing->current_route_id)); + + + connect(ui->delete_route, &QPushButton::clicked, this, [=]{ + auto current = ui->route_profiles->currentItem()->text(); + currentRouteProfiles = deleteItemFromList(currentRouteProfiles, current); + reloadProfileItems(); + }); + - QString geoipFn = NekoGui::FindCoreAsset("geoip.dat"); - QString geositeFn = NekoGui::FindCoreAsset("geosite.dat"); - // - const auto sourceStringsDomain = Qv2ray::components::GeositeReader::ReadGeoSiteFromFile(geositeFn); - directDomainTxt = new AutoCompleteTextEdit("geosite", sourceStringsDomain, this); - proxyDomainTxt = new AutoCompleteTextEdit("geosite", sourceStringsDomain, this); - blockDomainTxt = new AutoCompleteTextEdit("geosite", sourceStringsDomain, this); - // - const auto sourceStringsIP = Qv2ray::components::GeositeReader::ReadGeoSiteFromFile(geoipFn); - directIPTxt = new AutoCompleteTextEdit("geoip", sourceStringsIP, this); - proxyIPTxt = new AutoCompleteTextEdit("geoip", sourceStringsIP, this); - blockIPTxt = new AutoCompleteTextEdit("geoip", sourceStringsIP, this); - // - ui->directTxtLayout->addWidget(directDomainTxt, 0, 0); - ui->proxyTxtLayout->addWidget(proxyDomainTxt, 0, 0); - ui->blockTxtLayout->addWidget(blockDomainTxt, 0, 0); - // - ui->directIPLayout->addWidget(directIPTxt, 0, 0); - ui->proxyIPLayout->addWidget(proxyIPTxt, 0, 0); - ui->blockIPLayout->addWidget(blockIPTxt, 0, 0); - // - REFRESH_ACTIVE_ROUTING(NekoGui::dataStore->active_routing, NekoGui::dataStore->routing.get()) ADD_ASTERISK(this) } @@ -100,158 +115,24 @@ DialogManageRoutes::~DialogManageRoutes() { } void DialogManageRoutes::accept() { - D_C_SAVE_STRING(custom_route_global) - bool routeChanged = false; - if (NekoGui::dataStore->active_routing != active_routing) routeChanged = true; - SaveDisplayRouting(NekoGui::dataStore->routing.get()); - NekoGui::dataStore->active_routing = active_routing; - NekoGui::dataStore->routing->fn = ROUTES_PREFIX + NekoGui::dataStore->active_routing; - if (NekoGui::dataStore->routing->Save()) routeChanged = true; - // - QString info = "UpdateDataStore"; - if (routeChanged) info += "RouteChanged"; - MW_dialog_message(Dialog_DialogManageRoutes, info); - QDialog::accept(); -} - -// built in settings - -QList DialogManageRoutes::getBuiltInSchemes() { - QList list; - list.append(this->schemeToAction(tr("Bypass LAN and China"), routing_cn_lan)); - list.append(this->schemeToAction(tr("Global"), routing_global)); - return list; -} - -QAction *DialogManageRoutes::schemeToAction(const QString &name, const NekoGui::Routing &scheme) { - auto *action = new QAction(name, this); - connect(action, &QAction::triggered, [this, &scheme] { this->UpdateDisplayRouting((NekoGui::Routing *) &scheme, true); }); - return action; -} - -void DialogManageRoutes::UpdateDisplayRouting(NekoGui::Routing *conf, bool qv) { - // - directDomainTxt->setPlainText(conf->direct_domain); - proxyDomainTxt->setPlainText(conf->proxy_domain); - blockDomainTxt->setPlainText(conf->block_domain); - // - blockIPTxt->setPlainText(conf->block_ip); - directIPTxt->setPlainText(conf->direct_ip); - proxyIPTxt->setPlainText(conf->proxy_ip); - // - CACHE.custom_route = conf->custom; - ui->def_outbound->setCurrentText(conf->def_outbound); - // - if (qv) return; - // - ui->sniffing_mode->setCurrentIndex(conf->sniffing_mode); - ui->outbound_domain_strategy->setCurrentText(conf->outbound_domain_strategy); - ui->domainStrategyCombo->setCurrentText(conf->domain_strategy); - ui->use_dns_object->setChecked(conf->use_dns_object); - ui->dns_object->setPlainText(conf->dns_object); - ui->dns_routing->setChecked(conf->dns_routing); - ui->remote_dns->setCurrentText(conf->remote_dns); - ui->remote_dns_strategy->setCurrentText(conf->remote_dns_strategy); - ui->direct_dns->setCurrentText(conf->direct_dns); - ui->direct_dns_strategy->setCurrentText(conf->direct_dns_strategy); - ui->dns_final_out->setCurrentText(conf->dns_final_out); -} - -void DialogManageRoutes::SaveDisplayRouting(NekoGui::Routing *conf) { - conf->direct_ip = directIPTxt->toPlainText(); - conf->direct_domain = directDomainTxt->toPlainText(); - conf->proxy_ip = proxyIPTxt->toPlainText(); - conf->proxy_domain = proxyDomainTxt->toPlainText(); - conf->block_ip = blockIPTxt->toPlainText(); - conf->block_domain = blockDomainTxt->toPlainText(); - conf->def_outbound = ui->def_outbound->currentText(); - conf->custom = CACHE.custom_route; - // - conf->sniffing_mode = ui->sniffing_mode->currentIndex(); - conf->domain_strategy = ui->domainStrategyCombo->currentText(); - conf->outbound_domain_strategy = ui->outbound_domain_strategy->currentText(); - conf->use_dns_object = ui->use_dns_object->isChecked(); - conf->dns_object = ui->dns_object->toPlainText(); - conf->dns_routing = ui->dns_routing->isChecked(); - conf->remote_dns = ui->remote_dns->currentText(); - conf->remote_dns_strategy = ui->remote_dns_strategy->currentText(); - conf->direct_dns = ui->direct_dns->currentText(); - conf->direct_dns_strategy = ui->direct_dns_strategy->currentText(); - conf->dns_final_out = ui->dns_final_out->currentText(); -} - -void DialogManageRoutes::on_load_save_clicked() { - auto w = new QDialog; - auto layout = new QVBoxLayout; - w->setLayout(layout); - auto lineEdit = new QLineEdit; - layout->addWidget(lineEdit); - auto list = new QListWidget; - layout->addWidget(list); - for (const auto &name: NekoGui::Routing::List()) { - list->addItem(name); + if (currentRouteProfiles.empty()) { + MessageBoxInfo("Invalid settings", "Routing profile cannot be empty"); + return; } - connect(list, &QListWidget::currentTextChanged, lineEdit, &QLineEdit::setText); - auto bottom = new QHBoxLayout; - layout->addLayout(bottom); - auto load = new QPushButton; - load->setText(tr("Load")); - bottom->addWidget(load); - auto save = new QPushButton; - save->setText(tr("Save")); - bottom->addWidget(save); - auto remove = new QPushButton; - remove->setText(tr("Remove")); - bottom->addWidget(remove); - auto cancel = new QPushButton; - cancel->setText(tr("Cancel")); - bottom->addWidget(cancel); - connect(load, &QPushButton::clicked, w, [=] { - auto fn = lineEdit->text(); - if (!fn.isEmpty()) { - auto r = std::make_unique(); - r->load_control_must = true; - r->fn = ROUTES_PREFIX + fn; - if (r->Load()) { - if (QMessageBox::question(nullptr, software_name, tr("Load routing: %1").arg(fn) + "\n" + r->DisplayRouting()) == QMessageBox::Yes) { - REFRESH_ACTIVE_ROUTING(fn, r.get()) // temp save to the window - w->accept(); - } - } - } - }); - connect(save, &QPushButton::clicked, w, [=] { - auto fn = lineEdit->text(); - if (!fn.isEmpty()) { - auto r = std::make_unique(); - SaveDisplayRouting(r.get()); - r->fn = ROUTES_PREFIX + fn; - if (QMessageBox::question(nullptr, software_name, tr("Save routing: %1").arg(fn) + "\n" + r->DisplayRouting()) == QMessageBox::Yes) { - r->Save(); - REFRESH_ACTIVE_ROUTING(fn, r.get()) - w->accept(); - } - } - }); - connect(remove, &QPushButton::clicked, w, [=] { - auto fn = lineEdit->text(); - if (!fn.isEmpty() && NekoGui::Routing::List().length() > 1) { - if (QMessageBox::question(nullptr, software_name, tr("Remove routing: %1").arg(fn)) == QMessageBox::Yes) { - QFile f(ROUTES_PREFIX + fn); - f.remove(); - if (NekoGui::dataStore->active_routing == fn) { - NekoGui::Routing::SetToActive(NekoGui::Routing::List().first()); - REFRESH_ACTIVE_ROUTING(NekoGui::dataStore->active_routing, NekoGui::dataStore->routing.get()) - } - w->accept(); - } - } - }); - connect(cancel, &QPushButton::clicked, w, &QDialog::accept); - connect(list, &QListWidget::itemDoubleClicked, this, [=](QListWidgetItem *item) { - lineEdit->setText(item->text()); - emit load->clicked(); - }); - w->exec(); - w->deleteLater(); -} + + NekoGui::dataStore->routing->sniffing_mode = ui->sniffing_mode->currentIndex(); + NekoGui::dataStore->routing->domain_strategy = ui->domainStrategyCombo->currentText(); + NekoGui::dataStore->routing->outbound_domain_strategy = ui->outbound_domain_strategy->currentText(); + NekoGui::dataStore->routing->use_dns_object = ui->use_dns_object->isChecked(); + NekoGui::dataStore->routing->dns_object = ui->dns_object->toPlainText(); + NekoGui::dataStore->routing->dns_routing = ui->dns_routing->isChecked(); + NekoGui::dataStore->routing->remote_dns = ui->remote_dns->currentText(); + NekoGui::dataStore->routing->remote_dns_strategy = ui->remote_dns_strategy->currentText(); + NekoGui::dataStore->routing->direct_dns = ui->direct_dns->currentText(); + NekoGui::dataStore->routing->direct_dns_strategy = ui->direct_dns_strategy->currentText(); + NekoGui::dataStore->routing->dns_final_out = ui->dns_final_out->currentText(); + + // TODO add mine + + QDialog::accept(); +} \ No newline at end of file diff --git a/ui/dialog_manage_routes.h b/ui/dialog_manage_routes.h index 0030622..b4c376a 100644 --- a/ui/dialog_manage_routes.h +++ b/ui/dialog_manage_routes.h @@ -23,37 +23,15 @@ public: private: Ui::DialogManageRoutes *ui; - struct { - QString custom_route; - QString custom_route_global; - } CACHE; - - QMenu *builtInSchemesMenu; - Qv2ray::ui::widgets::AutoCompleteTextEdit *directDomainTxt; - Qv2ray::ui::widgets::AutoCompleteTextEdit *proxyDomainTxt; - Qv2ray::ui::widgets::AutoCompleteTextEdit *blockDomainTxt; - // - Qv2ray::ui::widgets::AutoCompleteTextEdit *directIPTxt; - Qv2ray::ui::widgets::AutoCompleteTextEdit *blockIPTxt; - Qv2ray::ui::widgets::AutoCompleteTextEdit *proxyIPTxt; - // - NekoGui::Routing routing_cn_lan = NekoGui::Routing(1); - NekoGui::Routing routing_global = NekoGui::Routing(0); - // - QString title_base; - QString active_routing; + void reloadProfileItems(); + QList currentRouteProfiles; public slots: - void accept() override; - QList getBuiltInSchemes(); + void on_new_route_clicked(); - QAction *schemeToAction(const QString &name, const NekoGui::Routing &scheme); + void on_edit_route_clicked(); - void UpdateDisplayRouting(NekoGui::Routing *conf, bool qv); - - void SaveDisplayRouting(NekoGui::Routing *conf); - - void on_load_save_clicked(); + void on_delete_route_clicked(); }; diff --git a/ui/dialog_manage_routes.ui b/ui/dialog_manage_routes.ui index 1f2bdbe..6c07a8b 100644 --- a/ui/dialog_manage_routes.ui +++ b/ui/dialog_manage_routes.ui @@ -27,47 +27,18 @@ - - - - Qt::Vertical + + + + false - - - 20 - 40 - - - + - - - - Route sets + + + + Server Address Strategy - - - - - Mange route set - - - - - - - Custom Route (global) - - - - - - - Note: Other settings are independent for each route set. - - - - @@ -81,8 +52,15 @@ For sing-box, it sets inbound.domain_strategy - - + + + + Sniffing Mode + + + + + false @@ -107,40 +85,16 @@ For sing-box, it sets inbound.domain_strategy - - + + + + + - Sniffing Mode + Routing Profile - - - - Server Address Strategy - - - - - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - @@ -182,6 +136,11 @@ For more information, see the document "Configuration/DNS". true + + + 8.8.8.8 + + https://8.8.8.8/dns-query @@ -414,188 +373,44 @@ For more information, see the document "Configuration/DNS". - Simple Route + Route - + - - + + + Routing Profiles + + - - - - - Block - - - Qt::AlignCenter - - - - - - - - - - Direct - - - Qt::AlignCenter - - - - - - - Domain - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - Proxy - - - Qt::AlignCenter - - - - - - - - - - - - - IP - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - + - - - - - - 0 + + + + + + New - - 0 + + + + + + Edit - - 0 + + + + + + Delete - - 0 - - - - - - 0 - 0 - - - - Preset - - - QToolButton::InstantPopup - - - Qt::ToolButtonTextBesideIcon - - - - - - - Custom Route - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Default Outbound - - - - - - - - proxy - - - - - bypass - - - - - block - - - - - - - - + + + + @@ -628,8 +443,6 @@ For more information, see the document "Configuration/DNS". sniffing_mode domainStrategyCombo outbound_domain_strategy - load_save - custom_route_global_edit remote_dns remote_dns_strategy direct_dns @@ -640,9 +453,6 @@ For more information, see the document "Configuration/DNS". format_dns_object dns_document dns_object - preset - custom_route_edit - def_outbound diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 12c00e6..1739230 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -304,7 +304,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi r.load_control_must = true; r.fn = ROUTES_PREFIX + fn; if (r.Load()) { - if (QMessageBox::question(GetMessageBoxParent(), software_name, tr("Load routing and apply: %1").arg(fn) + "\n" + r.DisplayRouting()) == QMessageBox::Yes) { + if (QMessageBox::question(GetMessageBoxParent(), software_name, tr("Load routing and apply: %1").arg(fn) + "\n" ) == QMessageBox::Yes) { NekoGui::Routing::SetToActive(fn); if (NekoGui::dataStore->started_id >= 0) { neko_start(NekoGui::dataStore->started_id); @@ -1577,32 +1577,6 @@ void MainWindow::on_masterLogBrowser_customContextMenuRequested(const QPoint &po auto item = QInputDialog::getItem(GetMessageBoxParent(), tr("Save as route"), tr("Save \"%1\" as a routing rule?").arg(newStr), items, select, false, &ok); - if (ok) { - auto index = items.indexOf(item); - switch (index) { - case 0: - ADD_TO_CURRENT_ROUTE(proxy_ip, newStr); - break; - case 1: - ADD_TO_CURRENT_ROUTE(direct_ip, newStr); - break; - case 2: - ADD_TO_CURRENT_ROUTE(block_ip, newStr); - break; - case 3: - ADD_TO_CURRENT_ROUTE(proxy_domain, newStr); - break; - case 4: - ADD_TO_CURRENT_ROUTE(direct_domain, newStr); - break; - case 5: - ADD_TO_CURRENT_ROUTE(block_domain, newStr); - break; - default: - break; - } - MW_dialog_message("", "UpdateDataStore,RouteChanged"); - } }); menu->addAction(action_add_route); diff --git a/ui/widget/RouteItem.cpp b/ui/widget/RouteItem.cpp new file mode 100644 index 0000000..359f923 --- /dev/null +++ b/ui/widget/RouteItem.cpp @@ -0,0 +1,122 @@ +#include "RouteItem.h" +#include "ui_RouteItem.h" +#include "db/RouteEntity.h" +#include "db/Database.hpp" + +#define ADJUST_SIZE runOnUiThread([=] { adjustSize(); adjustPosition(mainwindow); }, this); + +int RouteItem::getIndexOf(const QString& name) const { + for (int i=0;iRules.size();i++) { + if (chain->Rules[i]->name == name) return i; + } + + return -1; +} + +QString get_outbound_name(int id) { + // -2 is direct -3 is block -4 is dns_out + if (id == -2) return "direct"; + if (id == -3) return "block"; + if (id == -4) return "dns_out"; + auto profiles = NekoGui::profileManager->profiles; + if (profiles.count(id)) return profiles[id]->bean->name; + return "INVALID OUTBOUND"; +} + +QStringList get_all_outbounds() { + QStringList res; + auto profiles = NekoGui::profileManager->profiles; + for (const auto &item: profiles) { + res.append(item.second->bean->name); + } + + return res; +} + +RouteItem::RouteItem(QWidget *parent, const std::shared_ptr& routeChain) + : QGroupBox(parent), ui(new Ui::RouteItem) { + ui->setupUi(this); + + // make a copy + chain = routeChain; + + for (const auto &item: chain->Rules) { + ui->route_items->addItem(item->name); + } + + QStringList outboundOptions = {"direct", "block", "dns_out"}; + outboundOptions << get_all_outbounds(); + + ui->rule_attr->addItems(NekoGui::RouteRule::get_attributes()); + ui->rule_out->addItems(outboundOptions); + ui->rule_attr_text->hide(); + ui->rule_attr_data->setTitle(""); + + connect(ui->rule_attr_selector, &QComboBox::currentTextChanged, this, [=](const QString& text){ + if (currentIndex == -1) return; + chain->Rules[currentIndex]->set_field_value(ui->rule_attr->currentText(), {text}); + }); + + connect(ui->rule_attr_text, &QTextEdit::textChanged, this, [=] { + if (currentIndex == -1) return; + auto currentVal = ui->rule_attr_text->toPlainText().split('\n'); + chain->Rules[currentIndex]->set_field_value(ui->rule_attr->currentText(), currentVal); + }); + + connect(ui->route_items, &QListWidget::itemClicked, this, [=](const QListWidgetItem *item) { + auto idx = getIndexOf(item->text()); + if (idx == -1) return; + currentIndex = idx; + auto ruleItem = chain->Rules[idx]; + ui->rule_out->setCurrentText(get_outbound_name(ruleItem->outboundID)); + }); + + connect(ui->rule_attr, &QComboBox::currentTextChanged, this, [=](const QString& text){ + if (currentIndex == -1) return; + ui->rule_attr_data->setTitle(text); + auto inputType = NekoGui::RouteRule::get_input_type(text); + switch (inputType) { + case NekoGui::trufalse: { + QStringList items = {"", "true", "false"}; + auto currentValPtr = chain->Rules[currentIndex]->get_current_value_bool(text); + QString currentVal = currentValPtr == nullptr ? "" : *currentValPtr ? "true" : "false"; + showSelectItem(items, currentVal); + break; + } + case NekoGui::select: { + auto items = NekoGui::RouteRule::get_values_for_field(text); + auto currentVal = chain->Rules[currentIndex]->get_current_value_string(text)[0]; + showSelectItem(items, currentVal); + break; + } + case NekoGui::text: { + auto currentItems = chain->Rules[currentIndex]->get_current_value_string(text); + showTextEnterItem(currentItems); + break; + } + } + }); +} + +RouteItem::~RouteItem() { + delete ui; +} + +void RouteItem::showSelectItem(const QStringList& items, const QString& currentItem) { + ui->rule_attr_text->hide(); + ui->rule_attr_selector->clear(); + ui->rule_attr_selector->show(); + QStringList fullItems = {""}; + fullItems << items; + ui->rule_attr_selector->addItems(fullItems); + ui->rule_attr_selector->setCurrentText(currentItem); + adjustSize(); +} + +void RouteItem::showTextEnterItem(const QStringList& items) { + ui->rule_attr_selector->hide(); + ui->rule_attr_text->clear(); + ui->rule_attr_text->show(); + ui->rule_attr_text->setText(items.join('\n')); + adjustSize(); +} diff --git a/ui/widget/RouteItem.h b/ui/widget/RouteItem.h new file mode 100644 index 0000000..cb04f2d --- /dev/null +++ b/ui/widget/RouteItem.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#include "db/RouteEntity.h" + +QT_BEGIN_NAMESPACE +namespace Ui { + class RouteItem; +} +QT_END_NAMESPACE + +class RouteItem : public QGroupBox { + Q_OBJECT + +public: + explicit RouteItem(QWidget *parent = nullptr, const std::shared_ptr& routeChain = nullptr); + ~RouteItem() override; + + std::shared_ptr chain; +signals: + void settingsChanged(const std::shared_ptr routeChain); + +private: + Ui::RouteItem *ui; + int currentIndex = -1; + + [[nodiscard]] int getIndexOf(const QString& name) const; + + void showSelectItem(const QStringList& items, const QString& currentItem); + + void showTextEnterItem(const QStringList& items); + +private slots: + void on_ok_button_clicked(); + void on_cancel_button_clicked(); + void on_new_route_item_clicked(); + void on_moveup_route_item_clicked(); + void on_movedown_route_item_clicked(); + void on_route_view_json_clicked(); +}; diff --git a/ui/widget/RouteItem.ui b/ui/widget/RouteItem.ui new file mode 100644 index 0000000..a9b2cd1 --- /dev/null +++ b/ui/widget/RouteItem.ui @@ -0,0 +1,153 @@ + + + RouteItem + + + + 0 + 0 + 780 + 728 + + + + GroupBox + + + + + + + + + Name + + + + + + + + + + View JSON + + + + + + + + + + Rules + + + + + + + + + + + + New + + + + + + + Move Up + + + + + + + Move Down + + + + + + + + + + Rule Attributes + + + + + + + + + Attribute + + + + + + + + + + Outbound + + + + + + + + + + + + + Name_Placeholder + + + + + + + + + + + + + + + + + + + + + Ok + + + + + + + Cancel + + + + + + + + + + + + + +