#include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "BrowserTab.h" #include "DatabaseManager.h" #include "DownloadBar.h" #include "PasswordHelper.h" #include "ThemeConfig.h" #include "VaultManager.h" MainWindow::MainWindow() : isDarkMode(true) { setAttribute(Qt::WA_DeleteOnClose); setupToolBar(); setupUserInterface(); setupKeyboardShortcuts(); applyApplicationTheme(true); darkModeAction->setChecked(true); connect(QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested, this, [this](QWebEngineDownloadRequest *download) { downloadBar->addDownload(download); download->accept(); }); resize(ThemeConfig::DefaultWindowWidth, ThemeConfig::DefaultWindowHeight); // Initial tab createNewTab(QUrl("qrc:/debug.html")); } void MainWindow::setupUserInterface() { tabs = new QTabWidget(this); tabs->setTabsClosable(false); // We use middle click tabs->setMovable(true); auto *central = new QWidget(this); auto *layout = new QVBoxLayout(central); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(tabs, 1); downloadBar = new DownloadBar(central); layout->addWidget(downloadBar); setCentralWidget(central); tabs->tabBar()->installEventFilter(this); connect(tabs, &QTabWidget::tabCloseRequested, this, &MainWindow::closeTabAtIndex); connect(tabs, &QTabWidget::currentChanged, this, &MainWindow::updateWindowStatus); } void MainWindow::setupToolBar() { // Create completer first to avoid crash in event filter completer = new QCompleter(this); completerModel = new QSqlQueryModel(this); completer->setModel(completerModel); completer->setCompletionColumn(0); completer->setCompletionMode(QCompleter::PopupCompletion); completer->setFilterMode(Qt::MatchContains); completer->setCaseSensitivity(Qt::CaseInsensitive); toolbar = new QToolBar(this); addToolBar(toolbar); address = new QLineEdit(this); address->setCompleter(completer); address->installEventFilter(this); toolbar->addWidget(address); bookmarkAction = toolbar->addAction("Star"); connect(bookmarkAction, &QAction::triggered, this, &MainWindow::toggleBookmark); passwordAction = toolbar->addAction("Autofill"); passwordAction->setToolTip("Fill saved credentials (Ctrl+Shift+L)"); connect(passwordAction, &QAction::triggered, this, &MainWindow::showPasswordMenu); passwordAction->setVisible(false); auto *addTabAction = toolbar->addAction("+"); connect(addTabAction, &QAction::triggered, [this]() { createNewTab(); }); devtoolsAction = toolbar->addAction("DevTools"); connect(devtoolsAction, &QAction::triggered, [this]() { if (auto *tab = currentTab()) { tab->setDevToolsVisible(!tab->devtools->isVisible()); } }); darkModeAction = toolbar->addAction("Dark Mode"); darkModeAction->setCheckable(true); connect(darkModeAction, &QAction::toggled, this, &MainWindow::applyApplicationTheme); connect(address, &QLineEdit::returnPressed, this, &MainWindow::navigateToAddressOrSearch); updateAddressCompleter(); } void MainWindow::updateAddressCompleter() { completerModel->setQuery("SELECT DISTINCT url FROM history ORDER BY last_visit DESC LIMIT 1000"); } void MainWindow::setupKeyboardShortcuts() { // New Tab auto *newTab = new QAction(this); newTab->setShortcut(QKeySequence::AddTab); connect(newTab, &QAction::triggered, [this]() { createNewTab(); }); addAction(newTab); // New Window auto *newWin = new QAction(this); newWin->setShortcut(QKeySequence::New); connect(newWin, &QAction::triggered, []() { (new MainWindow())->show(); }); addAction(newWin); // Close Tab auto *closeTab = new QAction(this); closeTab->setShortcut(QKeySequence::Close); connect(closeTab, &QAction::triggered, [this]() { closeTabAtIndex(tabs->currentIndex()); }); addAction(closeTab); // Zoom shortcuts auto *zoomIn = new QAction(this); zoomIn->setShortcut(QKeySequence::ZoomIn); connect(zoomIn, &QAction::triggered, [this]() { if (auto *tab = currentTab()) { tab->view->setZoomFactor(tab->view->zoomFactor() + 0.1); } }); addAction(zoomIn); auto *zoomOut = new QAction(this); zoomOut->setShortcut(QKeySequence::ZoomOut); connect(zoomOut, &QAction::triggered, [this]() { if (auto *tab = currentTab()) { tab->view->setZoomFactor(qMax(0.25, tab->view->zoomFactor() - 0.1)); } }); addAction(zoomOut); auto *zoomReset = new QAction(this); zoomReset->setShortcut(Qt::CTRL | Qt::Key_0); connect(zoomReset, &QAction::triggered, [this]() { if (auto *tab = currentTab()) { tab->view->setZoomFactor(1.0); } }); addAction(zoomReset); // Refresh auto *reload = new QAction(this); reload->setShortcuts({QKeySequence::Refresh, QKeySequence(Qt::CTRL | Qt::Key_R)}); connect(reload, &QAction::triggered, [this]() { if (auto *tab = currentTab()) { tab->view->reload(); } }); addAction(reload); // Tab cycling auto *nextTab = new QAction(this); nextTab->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab)); connect(nextTab, &QAction::triggered, [this]() { tabs->setCurrentIndex((tabs->currentIndex() + 1) % tabs->count()); }); addAction(nextTab); auto *prevTab = new QAction(this); prevTab->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab)); connect(prevTab, &QAction::triggered, [this]() { tabs->setCurrentIndex((tabs->currentIndex() - 1 + tabs->count()) % tabs->count()); }); addAction(prevTab); // Exit fullscreen auto *esc = new QAction(this); esc->setShortcut(Qt::Key_Escape); connect(esc, &QAction::triggered, [this]() { if (isFullScreen()) { toolbar->show(); tabs->tabBar()->show(); downloadBar->show(); showNormal(); } }); addAction(esc); // Bookmarking auto *bookmark = new QAction(this); bookmark->setShortcut(Qt::CTRL | Qt::Key_D); connect(bookmark, &QAction::triggered, this, &MainWindow::toggleBookmark); addAction(bookmark); // Passwords manual trigger auto *fillPass = new QAction(this); fillPass->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_L); connect(fillPass, &QAction::triggered, this, &MainWindow::showPasswordMenu); addAction(fillPass); } void MainWindow::createNewTab(const QUrl &url, bool focus) { auto *tab = new BrowserTab; tab->updateTabTheme(isDarkMode); tab->view->createTabCallback = [this]() { createNewTab(QUrl(), false); return qobject_cast(tabs->widget(tabs->count() - 1))->view; }; int index = tabs->addTab(tab, url.isEmpty() ? "Blank" : "Loading..."); connect(tab->view, &QWebEngineView::urlChanged, [this, tab](const QUrl &u) { if (currentTab() == tab) { address->setText(u.toString()); updateBookmarkIcon(); } DatabaseManager::instance().addHistoryEntry(u.toString(), tab->view->title()); updateAddressCompleter(); QString newHost = u.host(); if (!newHost.isEmpty()) { bool ok; qreal saved = DatabaseManager::instance().getDomainSetting(newHost, "zoom").toDouble(&ok); if (ok && saved > 0.0) { tab->view->setZoomFactor(saved); } } }); connect(tab->view, &QWebEngineView::titleChanged, [this, tab](const QString &title) { int idx = tabs->indexOf(tab); if (idx != -1) { tabs->setTabText(idx, title); if (currentTab() == tab) { setWindowTitle(title + " - Browser"); } } DatabaseManager::instance().addHistoryEntry(tab->view->url().toString(), title); updateAddressCompleter(); }); connect(tab->view->pageAction(QWebEnginePage::InspectElement), &QAction::triggered, [tab]() { tab->setDevToolsVisible(true); }); connect(tab->view->page(), &QWebEnginePage::fullScreenRequested, [this](QWebEngineFullScreenRequest req) { if (req.toggleOn()) { toolbar->hide(); tabs->tabBar()->hide(); downloadBar->hide(); showFullScreen(); } else { toolbar->show(); tabs->tabBar()->show(); downloadBar->show(); showNormal(); } req.accept(); }); connect(tab->view->page(), &QWebEnginePage::permissionRequested, [this](QWebEnginePermission req) { QString type; switch (req.permissionType()) { case QWebEnginePermission::PermissionType::MediaVideoCapture: type = "Camera"; break; case QWebEnginePermission::PermissionType::MediaAudioCapture: type = "Microphone"; break; case QWebEnginePermission::PermissionType::Geolocation: type = "Location"; break; case QWebEnginePermission::PermissionType::Notifications: type = "Notifications"; break; default: type = "Resources"; break; } auto btn = QMessageBox::question(this, "Permission", QString("%1 wants to use your %2. Allow?").arg(req.origin().toString(), type)); (btn == QMessageBox::Yes) ? req.grant() : req.deny(); }); connect(tab->passwordHelper, &PasswordHelper::savePasswordRequested, [this](const QString &origin, const QString &username, const QString &password) { if (!VaultManager::instance().isUnlocked()) { return; } auto existing = VaultManager::instance().getPasswords(origin); bool foundUser = false; bool samePassword = false; for (const auto &entry : existing) { if (entry.username == username) { foundUser = true; if (entry.password == password) { samePassword = true; } break; } } if (samePassword) { return; } QString msg = foundUser ? QString("Do you want to update the password for %1 (User: %2)?").arg(origin, username) : QString("Do you want to save the password for %1 (User: %2)?").arg(origin, username); auto btn = QMessageBox::question(this, foundUser ? "Update Password" : "Save Password", msg); if (btn == QMessageBox::Yes) { VaultManager::instance().savePassword(origin, username, password); updatePasswordIcon(); } }); connect(tab->view, &QWebEngineView::loadFinished, [this, tab](bool ok) { if (ok) { QString host = tab->view->url().host(); if (!host.isEmpty()) { tab->setLastHost(host); bool zoomOk; qreal saved = DatabaseManager::instance().getDomainSetting(host, "zoom").toDouble(&zoomOk); if (zoomOk && saved > 0.0) { tab->view->setZoomFactor(saved); } } if (!VaultManager::instance().isUnlocked()) { return; } updatePasswordIcon(); auto creds = VaultManager::instance().getPasswords(host); if (creds.size() == 1) { auto entry = creds.first(); injectAutofill(tab, entry.username, entry.password); } } }); if (!url.isEmpty()) { tab->view->load(url); } if (focus) { tabs->setCurrentIndex(index); address->setFocus(); } } void MainWindow::closeTabAtIndex(int index) { if (tabs->count() > 1) { QWidget *w = tabs->widget(index); tabs->removeTab(index); w->deleteLater(); } else { close(); } } void MainWindow::navigateToAddressOrSearch() { if (auto *tab = currentTab()) { QString text = address->text().trimmed(); if (text.isEmpty()) { return; } bool isSearch = text.contains(' ') || (!text.contains('.') && !text.contains("://") && text != "localhost"); if (isSearch) { tab->view->load(QUrl("https://duckduckgo.com/?q=" + QUrl::toPercentEncoding(text))); } else { if (!text.contains("://")) { text = "https://" + text; } tab->view->load(QUrl(text)); } address->clearFocus(); } } void MainWindow::applyApplicationTheme(bool dark) { isDarkMode = dark; // Set color scheme first so that any QWebEnginePages created or reloaded // afterwards pick up the correct prefers-color-scheme media query value. QGuiApplication::styleHints()->setColorScheme(dark ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light); QPalette p; if (dark) { p.setColor(QPalette::Window, ThemeConfig::Dark::Window); p.setColor(QPalette::WindowText, ThemeConfig::Dark::WindowText); p.setColor(QPalette::Base, ThemeConfig::Dark::Base); p.setColor(QPalette::AlternateBase, ThemeConfig::Dark::AlternateBase); p.setColor(QPalette::ToolTipBase, ThemeConfig::Dark::ToolTipBase); p.setColor(QPalette::ToolTipText, ThemeConfig::Dark::ToolTipText); p.setColor(QPalette::Text, ThemeConfig::Dark::Text); p.setColor(QPalette::Button, ThemeConfig::Dark::Button); p.setColor(QPalette::ButtonText, ThemeConfig::Dark::ButtonText); p.setColor(QPalette::BrightText, ThemeConfig::Dark::BrightText); p.setColor(QPalette::Link, ThemeConfig::Dark::Link); p.setColor(QPalette::Highlight, ThemeConfig::Dark::Highlight); p.setColor(QPalette::HighlightedText, ThemeConfig::Dark::HighlightedText); } else { p = style()->standardPalette(); } qApp->setPalette(p); this->setPalette(p); for (int i = 0; i < tabs->count(); ++i) { if (auto *tab = qobject_cast(tabs->widget(i))) { tab->updateTabTheme(dark); // Reload so the page re-evaluates prefers-color-scheme with the new // value. tab->view->reload(); } } } void MainWindow::updateWindowStatus() { if (auto *tab = currentTab()) { address->setText(tab->view->url().toString()); setWindowTitle(tabs->tabText(tabs->currentIndex()) + " - Browser"); updateBookmarkIcon(); updatePasswordIcon(); } } void MainWindow::toggleBookmark() { if (auto *tab = currentTab()) { QString url = tab->view->url().toString(); QString title = tab->view->title(); if (DatabaseManager::instance().isBookmarked(url)) { DatabaseManager::instance().removeBookmark(url); } else { DatabaseManager::instance().addBookmark(url, title); } updateBookmarkIcon(); } } void MainWindow::updateBookmarkIcon() { if (auto *tab = currentTab()) { bool bookmarked = DatabaseManager::instance().isBookmarked(tab->view->url().toString()); bookmarkAction->setText(bookmarked ? "★" : "☆"); } } void MainWindow::updatePasswordIcon() { if (auto *tab = currentTab()) { QString host = tab->view->url().host(); auto creds = VaultManager::instance().getPasswords(host); passwordAction->setVisible(!creds.isEmpty()); } } void MainWindow::showPasswordMenu() { if (auto *tab = currentTab()) { QString host = tab->view->url().host(); auto creds = VaultManager::instance().getPasswords(host); if (creds.isEmpty()) { return; } QMenu menu(this); for (const auto &entry : creds) { auto *action = menu.addAction(entry.username); connect(action, &QAction::triggered, [this, tab, entry]() { injectAutofill(tab, entry.username, entry.password); }); } menu.exec(QCursor::pos()); } } void MainWindow::injectAutofill(BrowserTab *tab, const QString &username, const QString &password) { QString user = username; user.replace("\\", "\\\\"); user.replace("\"", "\\\""); QString pass = password; pass.replace("\\", "\\\\"); pass.replace("\"", "\\\""); QFile scriptFile(":/js/autofill.js"); if (scriptFile.open(QIODevice::ReadOnly)) { QString js = QString::fromUtf8(scriptFile.readAll()); js.replace("%1", user); js.replace("%2", pass); tab->view->page()->runJavaScript(js); } else { qCritical() << "Failed to load autofill.js from resources!"; } } BrowserTab *MainWindow::currentTab() const { return qobject_cast(tabs->currentWidget()); } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (address && obj == address && event->type() == QEvent::FocusIn) { updateAddressCompleter(); } if (tabs && obj == tabs->tabBar() && event->type() == QEvent::MouseButtonRelease) { auto *mouse = static_cast(event); if (mouse->button() == Qt::MiddleButton) { int index = tabs->tabBar()->tabAt(mouse->pos()); if (index != -1) { closeTabAtIndex(index); return true; } } } return QMainWindow::eventFilter(obj, event); }