Skip to content

Commit 092d18d

Browse files
author
oak
committed
feat: add custom quick-link buttons (3 buttons with custom names + URLs)
- Add quick_link_1/2/3 and quick_link_name_1/2/3 fields to SettingsRepo with DB persistence - Add Quick Links group to Basic Settings dialog (Settings → Basic Settings → Quick Links) - Add 3 quick-link buttons to main window toolbar with custom names - Click handlers open URLs in browser - refreshQuickLinkButtons() updates button texts immediately after save
1 parent 2e7182b commit 092d18d

8 files changed

Lines changed: 332 additions & 66 deletions

File tree

include/database/SettingsRepo.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,14 @@ namespace Configs {
9999
QStringList log_exclude_keyword = {};
100100
QStringList log_exclude_regex = {};
101101
bool log_auto_scroll = true;
102-
bool start_minimal = false;
102+
// Quick links (custom buttons on the main window)
103+
QString quick_link_1 = "";
104+
QString quick_link_2 = "";
105+
QString quick_link_3 = "";
106+
QString quick_link_name_1 = "";
107+
QString quick_link_name_2 = "";
108+
QString quick_link_name_3 = "";
109+
bool start_minimal = false;
103110
int max_log_line = 200;
104111
// On-disk diagnostic log only; log_level is the core's browser verbosity.
105112
QString log_file_level = "debug";

include/ui/mainwindow.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ namespace Configs_sys {
4949
}
5050

5151
class TrayProfileSelector;
52+
class RoutingQuickMenu;
5253
class TrayOtpCodes;
5354
class TestRunner;
5455

@@ -195,9 +196,13 @@ private slots:
195196

196197
void on_menu_resolve_domain_triggered();
197198

198-
void on_menu_update_subscription_triggered();
199+
void on_menu_update_subscription_triggered();
199200

200-
void on_profilesTableView_doubleClicked(const QModelIndex &index);
201+
void on_toolButton_link1_clicked();
202+
void on_toolButton_link2_clicked();
203+
void on_toolButton_link3_clicked();
204+
205+
void on_profilesTableView_doubleClicked(const QModelIndex &index);
201206

202207
void on_profilesTableView_customContextMenuRequested(const QPoint &pos);
203208

@@ -215,6 +220,11 @@ private slots:
215220
QMenu *trayMenu = nullptr; // tray context menu
216221
QPointer<TrayProfileSelector> traySelector;
217222
void openTraySelector(bool routing);
223+
QPointer<RoutingQuickMenu> routingQuickMenu;
224+
void openRoutingQuickMenu(const QPoint &globalPos);
225+
// Refresh the routing segment of the status bar from the active profile.
226+
void refreshRoutingStatus();
227+
void refreshQuickLinkButtons(); // Update quick link button texts from settings
218228
QPointer<TrayOtpCodes> trayOtpCodes;
219229
void openTrayOtpCodes();
220230
QShortcut *shortcut_esc = new QShortcut(QKeySequence::Cancel, this);

include/ui/setting/dialog_basic_settings.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ public slots:
2424
private:
2525
Ui::DialogBasicSettings *ui;
2626

27+
// Quick link fields (created in code, not in .ui)
28+
QLineEdit *quick_link_1 = nullptr;
29+
QLineEdit *quick_link_2 = nullptr;
30+
QLineEdit *quick_link_3 = nullptr;
31+
QLineEdit *quick_link_name_1 = nullptr;
32+
QLineEdit *quick_link_name_2 = nullptr;
33+
QLineEdit *quick_link_name_3 = nullptr;
34+
2735
void applyRegexHighlighting();
2836

2937
// Downloads an Xray geo asset (`fileName`, e.g. "geoip.dat") from `url` into the

include/ui/setting/dialog_basic_settings.ui

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,79 @@ QTabBar::tab:hover:!selected {
321321
</layout>
322322
</item>
323323
</layout>
324-
</widget>
325-
</item>
326-
<item>
327-
<widget class="QWidget" name="horizontalWidget_1" native="true">
324+
</widget>
325+
</item>
326+
<item>
327+
<widget class="QGroupBox" name="groupBox_quicklinks">
328+
<property name="title">
329+
<string>Quick Links</string>
330+
</property>
331+
<layout class="QGridLayout" name="gridLayout_quicklinks">
332+
<item row="0" column="0">
333+
<widget class="QLabel" name="label_quicklink_name_1">
334+
<property name="text">
335+
<string>Link 1 Name</string>
336+
</property>
337+
</widget>
338+
</item>
339+
<item row="0" column="1">
340+
<widget class="QLineEdit" name="quick_link_name_1"/>
341+
</item>
342+
<item row="1" column="0">
343+
<widget class="QLabel" name="label_quicklink_1">
344+
<property name="text">
345+
<string>Link 1 URL</string>
346+
</property>
347+
</widget>
348+
</item>
349+
<item row="1" column="1">
350+
<widget class="QLineEdit" name="quick_link_1"/>
351+
</item>
352+
<item row="2" column="0">
353+
<widget class="QLabel" name="label_quicklink_name_2">
354+
<property name="text">
355+
<string>Link 2 Name</string>
356+
</property>
357+
</widget>
358+
</item>
359+
<item row="2" column="1">
360+
<widget class="QLineEdit" name="quick_link_name_2"/>
361+
</item>
362+
<item row="3" column="0">
363+
<widget class="QLabel" name="label_quicklink_2">
364+
<property name="text">
365+
<string>Link 2 URL</string>
366+
</property>
367+
</widget>
368+
</item>
369+
<item row="3" column="1">
370+
<widget class="QLineEdit" name="quick_link_2"/>
371+
</item>
372+
<item row="4" column="0">
373+
<widget class="QLabel" name="label_quicklink_name_3">
374+
<property name="text">
375+
<string>Link 3 Name</string>
376+
</property>
377+
</widget>
378+
</item>
379+
<item row="4" column="1">
380+
<widget class="QLineEdit" name="quick_link_name_3"/>
381+
</item>
382+
<item row="5" column="0">
383+
<widget class="QLabel" name="label_quicklink_3">
384+
<property name="text">
385+
<string>Link 3 URL</string>
386+
</property>
387+
</widget>
388+
</item>
389+
<item row="5" column="1">
390+
<widget class="QLineEdit" name="quick_link_3"/>
391+
</item>
392+
</layout>
393+
</widget>
394+
</item>
395+
<item>
396+
<widget class="QWidget" name="horizontalWidget_1" native="true">
328397
<property name="sizePolicy">
329398
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
330399
<horstretch>0</horstretch>

src/database/SettingsRepo.cpp

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -102,54 +102,60 @@ namespace Configs {
102102
};
103103

104104
stringMap = {
105-
{"user_agent2", &user_agent},
106-
{"test_url", &test_latency_url},
107-
{"inbound_address", &inbound_address},
108-
{"log_level", &log_level},
109-
{"log_file_level", &log_file_level},
110-
{"mux_protocol", &mux_protocol},
111-
{"fragment_implementation", &fragment_implementation},
112-
{"fragment_size", &fragment_size},
113-
{"fragment_sleep", &fragment_sleep},
114-
{"theme", &theme},
115-
{"custom_inbound", &custom_inbound},
116-
{"custom_route", &custom_route_global},
117-
{"font", &font},
118-
{"hk_mw", &hotkey_mainwindow},
119-
{"hk_group", &hotkey_group},
120-
{"hk_route", &hotkey_route},
121-
{"hk_spmenu", &hotkey_system_proxy_menu},
122-
{"hk_toggle", &hotkey_toggle_system_proxy},
123-
{"active_routing", &active_routing},
124-
{"mw_size", &mw_size},
125-
{"vpn_impl", &vpn_implementation},
126-
{"vpn_tun_ipv4_cidr", &vpn_tun_ipv4_cidr},
127-
{"vpn_tun_ipv6_cidr", &vpn_tun_ipv6_cidr},
128-
{"sub_custom_hwid_params", &sub_custom_hwid_params},
129-
{"splitter_state", &splitter_state},
130-
{"utlsFingerprint", &utlsFingerprint},
131-
{"core_box_clash_listen_addr", &core_box_clash_listen_addr},
132-
{"core_box_clash_api_secret", &core_box_clash_api_secret},
133-
{"core_box_underlying_dns", &core_box_underlying_dns},
134-
{"ntp_server_address", &ntp_server_address},
135-
{"ntp_interval", &ntp_interval},
136-
{"ntp_outbound", &ntp_outbound},
137-
{"dns_v4_resp", &dns_v4_resp},
138-
{"dns_v6_resp", &dns_v6_resp},
139-
{"redirect_listen_address", &redirect_listen_address},
140-
{"proxy_scheme", &proxy_scheme},
141-
{"main_window_geometry", &mainWindowGeometry},
142-
{"xray_log_level", &xray_log_level},
143-
{"xray_geoip_url", &xray_geoip_url},
144-
{"xray_geosite_url", &xray_geosite_url},
145-
{"remote_dns", &remote_dns},
146-
{"remote_dns_strategy", &remote_dns_strategy},
147-
{"direct_dns", &direct_dns},
148-
{"direct_dns_strategy", &direct_dns_strategy},
149-
{"dns_object", &dns_object},
150-
{"dns_final_out", &dns_final_out},
151-
{"domain_strategy", &resolve_domain_strategy},
152-
{"outbound_domain_strategy", &default_domain_strategy},
105+
{"user_agent2", &user_agent},
106+
{"test_url", &test_latency_url},
107+
{"inbound_address", &inbound_address},
108+
{"log_level", &log_level},
109+
{"log_file_level", &log_file_level},
110+
{"mux_protocol", &mux_protocol},
111+
{"fragment_implementation", &fragment_implementation},
112+
{"fragment_size", &fragment_size},
113+
{"fragment_sleep", &fragment_sleep},
114+
{"theme", &theme},
115+
{"custom_inbound", &custom_inbound},
116+
{"custom_route", &custom_route_global},
117+
{"font", &font},
118+
{"hk_mw", &hotkey_mainwindow},
119+
{"hk_group", &hotkey_group},
120+
{"hk_route", &hotkey_route},
121+
{"hk_spmenu", &hotkey_system_proxy_menu},
122+
{"hk_toggle", &hotkey_toggle_system_proxy},
123+
{"active_routing", &active_routing},
124+
{"mw_size", &mw_size},
125+
{"vpn_impl", &vpn_implementation},
126+
{"vpn_tun_ipv4_cidr", &vpn_tun_ipv4_cidr},
127+
{"vpn_tun_ipv6_cidr", &vpn_tun_ipv6_cidr},
128+
{"sub_custom_hwid_params", &sub_custom_hwid_params},
129+
{"splitter_state", &splitter_state},
130+
{"utlsFingerprint", &utlsFingerprint},
131+
{"core_box_clash_listen_addr", &core_box_clash_listen_addr},
132+
{"core_box_clash_api_secret", &core_box_clash_api_secret},
133+
{"core_box_underlying_dns", &core_box_underlying_dns},
134+
{"ntp_server_address", &ntp_server_address},
135+
{"ntp_interval", &ntp_interval},
136+
{"ntp_outbound", &ntp_outbound},
137+
{"dns_v4_resp", &dns_v4_resp},
138+
{"dns_v6_resp", &dns_v6_resp},
139+
{"redirect_listen_address", &redirect_listen_address},
140+
{"proxy_scheme", &proxy_scheme},
141+
{"main_window_geometry", &mainWindowGeometry},
142+
{"xray_log_level", &xray_log_level},
143+
{"xray_geoip_url", &xray_geoip_url},
144+
{"xray_geosite_url", &xray_geosite_url},
145+
{"remote_dns", &remote_dns},
146+
{"remote_dns_strategy", &remote_dns_strategy},
147+
{"direct_dns", &direct_dns},
148+
{"direct_dns_strategy", &direct_dns_strategy},
149+
{"quick_link_name_1", &quick_link_name_1},
150+
{"quick_link_name_2", &quick_link_name_2},
151+
{"quick_link_name_3", &quick_link_name_3},
152+
{"quick_link_1", &quick_link_1},
153+
{"quick_link_2", &quick_link_2},
154+
{"quick_link_3", &quick_link_3},
155+
{"dns_object", &dns_object},
156+
{"dns_final_out", &dns_final_out},
157+
{"domain_strategy", &resolve_domain_strategy},
158+
{"outbound_domain_strategy", &default_domain_strategy},
153159
{"simple_dl_url", &simple_dl_url},
154160
{"warp_private_key", &warp_private_key},
155161
{"warp_public_key", &warp_public_key},

src/ui/mainWindow/mainwindow_events.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
#include "include/ui/widget/TrayOtpCodes.hpp"
1010
#include "include/ui/widget/TrayProfileSelector.hpp"
11+
#include "include/ui/widget/RoutingQuickMenu.hpp"
12+
#include "include/ui/setting/RouteItem.h"
1113

1214
void MainWindow::trayClickEvent() {
1315
constexpr qint64 recentlyActiveMs = 350;
@@ -178,6 +180,101 @@ void MainWindow::openTrayOtpCodes() {
178180
trayOtpCodes->popupAt(QCursor::pos());
179181
}
180182

183+
void MainWindow::on_toolButton_link1_clicked() {
184+
const auto &url = Configs::dataManager->settingsRepo->quick_link_1;
185+
if (url.isEmpty()) {
186+
QMessageBox::information(this, "Quick Link 1", "Set the URL in Settings → Basic Settings → Quick Links.");
187+
return;
188+
}
189+
QDesktopServices::openUrl(QUrl(url));
190+
}
191+
192+
void MainWindow::on_toolButton_link2_clicked() {
193+
const auto &url = Configs::dataManager->settingsRepo->quick_link_2;
194+
if (url.isEmpty()) {
195+
QMessageBox::information(this, "Quick Link 2", "Set the URL in Settings → Basic Settings → Quick Links.");
196+
return;
197+
}
198+
QDesktopServices::openUrl(QUrl(url));
199+
}
200+
201+
void MainWindow::on_toolButton_link3_clicked() {
202+
const auto &url = Configs::dataManager->settingsRepo->quick_link_3;
203+
if (url.isEmpty()) {
204+
QMessageBox::information(this, "Quick Link 3", "Set the URL in Settings → Basic Settings → Quick Links.");
205+
return;
206+
}
207+
QDesktopServices::openUrl(QUrl(url));
208+
}
209+
210+
void MainWindow::refreshRoutingStatus() {
211+
if (auto *label = findChild<QLabel *>(QStringLiteral("routingStatus")))
212+
setStatusText(label, RoutingQuickMenu::statusSummary());
213+
}
214+
215+
void MainWindow::openRoutingQuickMenu(const QPoint &globalPos) {
216+
if (routingQuickMenu) routingQuickMenu->close();
217+
218+
// Applying a routing change means regenerating the config, so a running
219+
// profile has to be restarted for it to take effect.
220+
const auto applyToRunningProfile = [this] {
221+
refreshRoutingStatus();
222+
if (Configs::dataManager->settingsRepo->started_id >= 0)
223+
profile_start(Configs::dataManager->settingsRepo->started_id);
224+
};
225+
const auto withActiveProfile = [applyToRunningProfile](const std::function<void(Configs::RouteProfile &)> &change) {
226+
auto profile = Configs::dataManager->routesRepo->GetRouteProfile(
227+
Configs::dataManager->settingsRepo->current_route_id);
228+
if (!profile) return;
229+
change(*profile);
230+
Configs::dataManager->routesRepo->Save(profile);
231+
applyToRunningProfile();
232+
};
233+
234+
RoutingQuickMenu::Callbacks cb;
235+
cb.setDefaultOutbound = [withActiveProfile](int outboundID) {
236+
withActiveProfile([outboundID](Configs::RouteProfile &profile) { profile.defaultOutboundID = outboundID; });
237+
};
238+
cb.setApplyProfileRules = [withActiveProfile](bool enabled) {
239+
withActiveProfile([enabled](Configs::RouteProfile &profile) { profile.applyProfileRules = enabled; });
240+
};
241+
cb.openProfile = [this, applyToRunningProfile] {
242+
auto profile = Configs::dataManager->routesRepo->GetRouteProfile(
243+
Configs::dataManager->settingsRepo->current_route_id);
244+
// Raw profiles have their own editor; the structured one cannot show them.
245+
if (!profile || profile->isRaw) {
246+
on_menu_routing_settings_triggered();
247+
return;
248+
}
249+
if (dialog_is_using) return;
250+
dialog_is_using = true;
251+
auto *editor = new RouteItem(this, profile);
252+
connect(editor, &RouteItem::settingsChanged, this,
253+
[applyToRunningProfile](const std::shared_ptr<Configs::RouteProfile> &edited) {
254+
Configs::dataManager->routesRepo->Save(edited);
255+
applyToRunningProfile();
256+
});
257+
connect(editor, &QDialog::finished, this, [this, editor] {
258+
editor->deleteLater();
259+
dialog_is_using = false;
260+
});
261+
editor->show();
262+
};
263+
cb.manageProfiles = [this] { on_menu_routing_settings_triggered(); };
264+
265+
routingQuickMenu = new RoutingQuickMenu(cb, this);
266+
routingQuickMenu->popupAt(globalPos);
267+
}
268+
269+
void MainWindow::refreshQuickLinkButtons() {
270+
auto *btn1 = findChild<QToolButton*>("linkBtn1");
271+
auto *btn2 = findChild<QToolButton*>("linkBtn2");
272+
auto *btn3 = findChild<QToolButton*>("linkBtn3");
273+
if (btn1) btn1->setText(Configs::dataManager->settingsRepo->quick_link_name_1.isEmpty() ? tr("Link 1") : Configs::dataManager->settingsRepo->quick_link_name_1);
274+
if (btn2) btn2->setText(Configs::dataManager->settingsRepo->quick_link_name_2.isEmpty() ? tr("Link 2") : Configs::dataManager->settingsRepo->quick_link_name_2);
275+
if (btn3) btn3->setText(Configs::dataManager->settingsRepo->quick_link_name_3.isEmpty() ? tr("Link 3") : Configs::dataManager->settingsRepo->quick_link_name_3);
276+
}
277+
181278
void MainWindow::keyPressEvent(QKeyEvent *event) {
182279
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
183280
if (!qobject_cast<QLineEdit *>(QApplication::focusWidget())) {
@@ -208,6 +305,12 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
208305
} else if (obj == ui->tabWidget && mouseEvent->button() == Qt::RightButton) {
209306
on_tabWidget_customContextMenuRequested(mouseEvent->position().toPoint());
210307
return true;
308+
} else if (mouseEvent->button() == Qt::LeftButton) {
309+
if (auto *segment = qobject_cast<QFrame *>(obj);
310+
segment && segment->objectName() == QStringLiteral("routingStatusButton")) {
311+
openRoutingQuickMenu(segment->mapToGlobal(QPoint(segment->width() / 2, 0)));
312+
return true;
313+
}
211314
}
212315
} else if (type == QEvent::MouseButtonDblClick) {
213316
if (obj == ui->splitter) {

0 commit comments

Comments
 (0)