.clang-format
.clang-tidy
.gitignore
.vimrc
BrowserTab.cpp
BrowserTab.h
BrowserView.cpp
BrowserView.h
CMakeLists.txt
DatabaseManager.cpp
DatabaseManager.h
DownloadBar.cpp
DownloadBar.h
DownloadWidget.cpp
DownloadWidget.h
MainWindow.cpp
MainWindow.h
Makefile
MasterPasswordDialog.cpp
MasterPasswordDialog.h
PasswordHelper.cpp
PasswordHelper.h
README.md
ThemeConfig.h
VaultManager.cpp
VaultManager.h
browser.desktop
browser.qrc
compile_commands.json
main.cpp
MainWindow.cpp
raw
1#include "MainWindow.h"
2#include <QAction>
3#include <QApplication>
4#include <QCompleter>
5#include <QCursor>
6#include <QDebug>
7#include <QFile>
8#include <QLineEdit>
9#include <QMenu>
10#include <QMessageBox>
11#include <QMouseEvent>
12#include <QPalette>
13#include <QSqlQueryModel>
14#include <QStandardPaths>
15#include <QStyle>
16#include <QStyleHints>
17#include <QTabBar>
18#include <QTabWidget>
19#include <QToolBar>
20#include <QVBoxLayout>
21#include <QWebEngineDownloadRequest>
22#include <QWebEngineFullScreenRequest>
23#include <QWebEnginePermission>
24#include <QWebEngineProfile>
25#include "BrowserTab.h"
26#include "DatabaseManager.h"
27#include "DownloadBar.h"
28#include "PasswordHelper.h"
29#include "ThemeConfig.h"
30#include "VaultManager.h"
31
32MainWindow::MainWindow() : isDarkMode(true) {
33 setAttribute(Qt::WA_DeleteOnClose);
34 setupToolBar();
35 setupUserInterface();
36 setupKeyboardShortcuts();
37
38 applyApplicationTheme(true);
39 darkModeAction->setChecked(true);
40
41 connect(QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested, this, [this](QWebEngineDownloadRequest *download) {
42 downloadBar->addDownload(download);
43 download->accept();
44 });
45
46 resize(ThemeConfig::DefaultWindowWidth, ThemeConfig::DefaultWindowHeight);
47
48 // Initial tab
49 createNewTab(QUrl("qrc:/debug.html"));
50}
51
52void MainWindow::setupUserInterface() {
53 tabs = new QTabWidget(this);
54 tabs->setTabsClosable(false); // We use middle click
55 tabs->setMovable(true);
56
57 auto *central = new QWidget(this);
58 auto *layout = new QVBoxLayout(central);
59 layout->setContentsMargins(0, 0, 0, 0);
60 layout->setSpacing(0);
61 layout->addWidget(tabs, 1);
62
63 downloadBar = new DownloadBar(central);
64 layout->addWidget(downloadBar);
65
66 setCentralWidget(central);
67
68 tabs->tabBar()->installEventFilter(this);
69
70 connect(tabs, &QTabWidget::tabCloseRequested, this, &MainWindow::closeTabAtIndex);
71 connect(tabs, &QTabWidget::currentChanged, this, &MainWindow::updateWindowStatus);
72}
73
74void MainWindow::setupToolBar() {
75 // Create completer first to avoid crash in event filter
76 completer = new QCompleter(this);
77 completerModel = new QSqlQueryModel(this);
78 completer->setModel(completerModel);
79 completer->setCompletionColumn(0);
80 completer->setCompletionMode(QCompleter::PopupCompletion);
81 completer->setFilterMode(Qt::MatchContains);
82 completer->setCaseSensitivity(Qt::CaseInsensitive);
83
84 toolbar = new QToolBar(this);
85 addToolBar(toolbar);
86
87 address = new QLineEdit(this);
88 address->setCompleter(completer);
89 address->installEventFilter(this);
90 toolbar->addWidget(address);
91
92 bookmarkAction = toolbar->addAction("Star");
93 connect(bookmarkAction, &QAction::triggered, this, &MainWindow::toggleBookmark);
94
95 passwordAction = toolbar->addAction("Autofill");
96 passwordAction->setToolTip("Fill saved credentials (Ctrl+Shift+L)");
97 connect(passwordAction, &QAction::triggered, this, &MainWindow::showPasswordMenu);
98 passwordAction->setVisible(false);
99
100 auto *addTabAction = toolbar->addAction("+");
101 connect(addTabAction, &QAction::triggered, [this]() { createNewTab(); });
102
103 devtoolsAction = toolbar->addAction("DevTools");
104 connect(devtoolsAction, &QAction::triggered, [this]() {
105 if (auto *tab = currentTab()) {
106 tab->setDevToolsVisible(!tab->devtools->isVisible());
107 }
108 });
109
110 darkModeAction = toolbar->addAction("Dark Mode");
111 darkModeAction->setCheckable(true);
112 connect(darkModeAction, &QAction::toggled, this, &MainWindow::applyApplicationTheme);
113
114 connect(address, &QLineEdit::returnPressed, this, &MainWindow::navigateToAddressOrSearch);
115 updateAddressCompleter();
116}
117
118void MainWindow::updateAddressCompleter() {
119 completerModel->setQuery("SELECT DISTINCT url FROM history ORDER BY last_visit DESC LIMIT 1000");
120}
121
122void MainWindow::setupKeyboardShortcuts() {
123 // New Tab
124 auto *newTab = new QAction(this);
125 newTab->setShortcut(QKeySequence::AddTab);
126 connect(newTab, &QAction::triggered, [this]() { createNewTab(); });
127 addAction(newTab);
128
129 // New Window
130 auto *newWin = new QAction(this);
131 newWin->setShortcut(QKeySequence::New);
132 connect(newWin, &QAction::triggered, []() { (new MainWindow())->show(); });
133 addAction(newWin);
134
135 // Close Tab
136 auto *closeTab = new QAction(this);
137 closeTab->setShortcut(QKeySequence::Close);
138 connect(closeTab, &QAction::triggered, [this]() { closeTabAtIndex(tabs->currentIndex()); });
139 addAction(closeTab);
140
141 // Zoom shortcuts
142 auto *zoomIn = new QAction(this);
143 zoomIn->setShortcut(QKeySequence::ZoomIn);
144 connect(zoomIn, &QAction::triggered, [this]() {
145 if (auto *tab = currentTab()) {
146 tab->view->setZoomFactor(tab->view->zoomFactor() + 0.1);
147 }
148 });
149 addAction(zoomIn);
150
151 auto *zoomOut = new QAction(this);
152 zoomOut->setShortcut(QKeySequence::ZoomOut);
153 connect(zoomOut, &QAction::triggered, [this]() {
154 if (auto *tab = currentTab()) {
155 tab->view->setZoomFactor(qMax(0.25, tab->view->zoomFactor() - 0.1));
156 }
157 });
158 addAction(zoomOut);
159
160 auto *zoomReset = new QAction(this);
161 zoomReset->setShortcut(Qt::CTRL | Qt::Key_0);
162 connect(zoomReset, &QAction::triggered, [this]() {
163 if (auto *tab = currentTab()) {
164 tab->view->setZoomFactor(1.0);
165 }
166 });
167 addAction(zoomReset);
168
169 // Refresh
170 auto *reload = new QAction(this);
171 reload->setShortcuts({QKeySequence::Refresh, QKeySequence(Qt::CTRL | Qt::Key_R)});
172 connect(reload, &QAction::triggered, [this]() {
173 if (auto *tab = currentTab()) {
174 tab->view->reload();
175 }
176 });
177 addAction(reload);
178
179 // Tab cycling
180 auto *nextTab = new QAction(this);
181 nextTab->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab));
182 connect(nextTab, &QAction::triggered, [this]() { tabs->setCurrentIndex((tabs->currentIndex() + 1) % tabs->count()); });
183 addAction(nextTab);
184
185 auto *prevTab = new QAction(this);
186 prevTab->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab));
187 connect(prevTab, &QAction::triggered, [this]() { tabs->setCurrentIndex((tabs->currentIndex() - 1 + tabs->count()) % tabs->count()); });
188 addAction(prevTab);
189
190 // Exit fullscreen
191 auto *esc = new QAction(this);
192 esc->setShortcut(Qt::Key_Escape);
193 connect(esc, &QAction::triggered, [this]() {
194 if (isFullScreen()) {
195 toolbar->show();
196 tabs->tabBar()->show();
197 downloadBar->show();
198 showNormal();
199 }
200 });
201 addAction(esc);
202
203 // Bookmarking
204 auto *bookmark = new QAction(this);
205 bookmark->setShortcut(Qt::CTRL | Qt::Key_D);
206 connect(bookmark, &QAction::triggered, this, &MainWindow::toggleBookmark);
207 addAction(bookmark);
208
209 // Passwords manual trigger
210 auto *fillPass = new QAction(this);
211 fillPass->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_L);
212 connect(fillPass, &QAction::triggered, this, &MainWindow::showPasswordMenu);
213 addAction(fillPass);
214}
215
216void MainWindow::createNewTab(const QUrl &url, bool focus) {
217 auto *tab = new BrowserTab;
218 tab->updateTabTheme(isDarkMode);
219
220 tab->view->createTabCallback = [this]() {
221 createNewTab(QUrl(), false);
222 return qobject_cast<BrowserTab *>(tabs->widget(tabs->count() - 1))->view;
223 };
224
225 int index = tabs->addTab(tab, url.isEmpty() ? "Blank" : "Loading...");
226
227 connect(tab->view, &QWebEngineView::urlChanged, [this, tab](const QUrl &u) {
228 if (currentTab() == tab) {
229 address->setText(u.toString());
230 updateBookmarkIcon();
231 }
232 DatabaseManager::instance().addHistoryEntry(u.toString(), tab->view->title());
233 updateAddressCompleter();
234
235 QString newHost = u.host();
236 if (!newHost.isEmpty()) {
237 bool ok;
238 qreal saved = DatabaseManager::instance().getDomainSetting(newHost, "zoom").toDouble(&ok);
239 if (ok && saved > 0.0) {
240 tab->view->setZoomFactor(saved);
241 }
242 }
243 });
244
245 connect(tab->view, &QWebEngineView::titleChanged, [this, tab](const QString &title) {
246 int idx = tabs->indexOf(tab);
247 if (idx != -1) {
248 tabs->setTabText(idx, title);
249 if (currentTab() == tab) {
250 setWindowTitle(title + " - Browser");
251 }
252 }
253 DatabaseManager::instance().addHistoryEntry(tab->view->url().toString(), title);
254 updateAddressCompleter();
255 });
256
257 connect(tab->view->pageAction(QWebEnginePage::InspectElement), &QAction::triggered, [tab]() { tab->setDevToolsVisible(true); });
258
259 connect(tab->view->page(), &QWebEnginePage::fullScreenRequested, [this](QWebEngineFullScreenRequest req) {
260 if (req.toggleOn()) {
261 toolbar->hide();
262 tabs->tabBar()->hide();
263 downloadBar->hide();
264 showFullScreen();
265 } else {
266 toolbar->show();
267 tabs->tabBar()->show();
268 downloadBar->show();
269 showNormal();
270 }
271 req.accept();
272 });
273
274 connect(tab->view->page(), &QWebEnginePage::permissionRequested, [this](QWebEnginePermission req) {
275 QString type;
276 switch (req.permissionType()) {
277 case QWebEnginePermission::PermissionType::MediaVideoCapture:
278 type = "Camera";
279 break;
280 case QWebEnginePermission::PermissionType::MediaAudioCapture:
281 type = "Microphone";
282 break;
283 case QWebEnginePermission::PermissionType::Geolocation:
284 type = "Location";
285 break;
286 case QWebEnginePermission::PermissionType::Notifications:
287 type = "Notifications";
288 break;
289 default:
290 type = "Resources";
291 break;
292 }
293
294 auto btn = QMessageBox::question(this, "Permission", QString("%1 wants to use your %2. Allow?").arg(req.origin().toString(), type));
295
296 (btn == QMessageBox::Yes) ? req.grant() : req.deny();
297 });
298
299 connect(tab->passwordHelper, &PasswordHelper::savePasswordRequested, [this](const QString &origin, const QString &username, const QString &password) {
300 if (!VaultManager::instance().isUnlocked()) {
301 return;
302 }
303
304 auto existing = VaultManager::instance().getPasswords(origin);
305 bool foundUser = false;
306 bool samePassword = false;
307
308 for (const auto &entry : existing) {
309 if (entry.username == username) {
310 foundUser = true;
311 if (entry.password == password) {
312 samePassword = true;
313 }
314 break;
315 }
316 }
317
318 if (samePassword) {
319 return;
320 }
321
322 QString msg = foundUser ? QString("Do you want to update the password for %1 (User: %2)?").arg(origin, username)
323 : QString("Do you want to save the password for %1 (User: %2)?").arg(origin, username);
324
325 auto btn = QMessageBox::question(this, foundUser ? "Update Password" : "Save Password", msg);
326
327 if (btn == QMessageBox::Yes) {
328 VaultManager::instance().savePassword(origin, username, password);
329 updatePasswordIcon();
330 }
331 });
332
333 connect(tab->view, &QWebEngineView::loadFinished, [this, tab](bool ok) {
334 if (ok) {
335 QString host = tab->view->url().host();
336 if (!host.isEmpty()) {
337 tab->setLastHost(host);
338 bool zoomOk;
339 qreal saved = DatabaseManager::instance().getDomainSetting(host, "zoom").toDouble(&zoomOk);
340 if (zoomOk && saved > 0.0) {
341 tab->view->setZoomFactor(saved);
342 }
343 }
344
345 if (!VaultManager::instance().isUnlocked()) {
346 return;
347 }
348 updatePasswordIcon();
349 auto creds = VaultManager::instance().getPasswords(host);
350 if (creds.size() == 1) {
351 auto entry = creds.first();
352 injectAutofill(tab, entry.username, entry.password);
353 }
354 }
355 });
356
357 if (!url.isEmpty()) {
358 tab->view->load(url);
359 }
360
361 if (focus) {
362 tabs->setCurrentIndex(index);
363 address->setFocus();
364 }
365}
366
367void MainWindow::closeTabAtIndex(int index) {
368 if (tabs->count() > 1) {
369 QWidget *w = tabs->widget(index);
370 tabs->removeTab(index);
371 w->deleteLater();
372 } else {
373 close();
374 }
375}
376
377void MainWindow::navigateToAddressOrSearch() {
378 if (auto *tab = currentTab()) {
379 QString text = address->text().trimmed();
380 if (text.isEmpty()) {
381 return;
382 }
383
384 bool isSearch = text.contains(' ') || (!text.contains('.') && !text.contains("://") && text != "localhost");
385
386 if (isSearch) {
387 tab->view->load(QUrl("https://duckduckgo.com/?q=" + QUrl::toPercentEncoding(text)));
388 } else {
389 if (!text.contains("://")) {
390 text = "https://" + text;
391 }
392 tab->view->load(QUrl(text));
393 }
394 address->clearFocus();
395 }
396}
397
398void MainWindow::applyApplicationTheme(bool dark) {
399 isDarkMode = dark;
400
401 // Set color scheme first so that any QWebEnginePages created or reloaded
402 // afterwards pick up the correct prefers-color-scheme media query value.
403 QGuiApplication::styleHints()->setColorScheme(dark ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light);
404
405 QPalette p;
406 if (dark) {
407 p.setColor(QPalette::Window, ThemeConfig::Dark::Window);
408 p.setColor(QPalette::WindowText, ThemeConfig::Dark::WindowText);
409 p.setColor(QPalette::Base, ThemeConfig::Dark::Base);
410 p.setColor(QPalette::AlternateBase, ThemeConfig::Dark::AlternateBase);
411 p.setColor(QPalette::ToolTipBase, ThemeConfig::Dark::ToolTipBase);
412 p.setColor(QPalette::ToolTipText, ThemeConfig::Dark::ToolTipText);
413 p.setColor(QPalette::Text, ThemeConfig::Dark::Text);
414 p.setColor(QPalette::Button, ThemeConfig::Dark::Button);
415 p.setColor(QPalette::ButtonText, ThemeConfig::Dark::ButtonText);
416 p.setColor(QPalette::BrightText, ThemeConfig::Dark::BrightText);
417 p.setColor(QPalette::Link, ThemeConfig::Dark::Link);
418 p.setColor(QPalette::Highlight, ThemeConfig::Dark::Highlight);
419 p.setColor(QPalette::HighlightedText, ThemeConfig::Dark::HighlightedText);
420 } else {
421 p = style()->standardPalette();
422 }
423
424 qApp->setPalette(p);
425 this->setPalette(p);
426
427 for (int i = 0; i < tabs->count(); ++i) {
428 if (auto *tab = qobject_cast<BrowserTab *>(tabs->widget(i))) {
429 tab->updateTabTheme(dark);
430 // Reload so the page re-evaluates prefers-color-scheme with the new
431 // value.
432 tab->view->reload();
433 }
434 }
435}
436
437void MainWindow::updateWindowStatus() {
438 if (auto *tab = currentTab()) {
439 address->setText(tab->view->url().toString());
440 setWindowTitle(tabs->tabText(tabs->currentIndex()) + " - Browser");
441 updateBookmarkIcon();
442 updatePasswordIcon();
443 }
444}
445
446void MainWindow::toggleBookmark() {
447 if (auto *tab = currentTab()) {
448 QString url = tab->view->url().toString();
449 QString title = tab->view->title();
450 if (DatabaseManager::instance().isBookmarked(url)) {
451 DatabaseManager::instance().removeBookmark(url);
452 } else {
453 DatabaseManager::instance().addBookmark(url, title);
454 }
455 updateBookmarkIcon();
456 }
457}
458
459void MainWindow::updateBookmarkIcon() {
460 if (auto *tab = currentTab()) {
461 bool bookmarked = DatabaseManager::instance().isBookmarked(tab->view->url().toString());
462 bookmarkAction->setText(bookmarked ? "★" : "☆");
463 }
464}
465
466void MainWindow::updatePasswordIcon() {
467 if (auto *tab = currentTab()) {
468 QString host = tab->view->url().host();
469 auto creds = VaultManager::instance().getPasswords(host);
470 passwordAction->setVisible(!creds.isEmpty());
471 }
472}
473
474void MainWindow::showPasswordMenu() {
475 if (auto *tab = currentTab()) {
476 QString host = tab->view->url().host();
477 auto creds = VaultManager::instance().getPasswords(host);
478 if (creds.isEmpty()) {
479 return;
480 }
481
482 QMenu menu(this);
483 for (const auto &entry : creds) {
484 auto *action = menu.addAction(entry.username);
485 connect(action, &QAction::triggered, [this, tab, entry]() { injectAutofill(tab, entry.username, entry.password); });
486 }
487 menu.exec(QCursor::pos());
488 }
489}
490
491void MainWindow::injectAutofill(BrowserTab *tab, const QString &username, const QString &password) {
492 QString user = username;
493 user.replace("\\", "\\\\");
494 user.replace("\"", "\\\"");
495 QString pass = password;
496 pass.replace("\\", "\\\\");
497 pass.replace("\"", "\\\"");
498
499 QFile scriptFile(":/js/autofill.js");
500 if (scriptFile.open(QIODevice::ReadOnly)) {
501 QString js = QString::fromUtf8(scriptFile.readAll());
502 js.replace("%1", user);
503 js.replace("%2", pass);
504 tab->view->page()->runJavaScript(js);
505 } else {
506 qCritical() << "Failed to load autofill.js from resources!";
507 }
508}
509
510BrowserTab *MainWindow::currentTab() const {
511 return qobject_cast<BrowserTab *>(tabs->currentWidget());
512}
513
514bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
515 if (address && obj == address && event->type() == QEvent::FocusIn) {
516 updateAddressCompleter();
517 }
518 if (tabs && obj == tabs->tabBar() && event->type() == QEvent::MouseButtonRelease) {
519 auto *mouse = static_cast<QMouseEvent *>(event);
520 if (mouse->button() == Qt::MiddleButton) {
521 int index = tabs->tabBar()->tabAt(mouse->pos());
522 if (index != -1) {
523 closeTabAtIndex(index);
524 return true;
525 }
526 }
527 }
528 return QMainWindow::eventFilter(obj, event);
529}