From 1e2302be2b47fbeb340db7482ed8476c829a7f66 Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Sun, 9 Mar 2025 18:15:12 +0530 Subject: Add browser manager + add multitab management (next/prev/close/new) --- TODO.org | 19 ++++ include/BrowserManager.hpp | 39 +++++++ include/MainWindow.hpp | 6 +- spec/BrowserManagerSpec.cpp | 271 ++++++++++++++++++++++++++++++++++++++++++++ src/BrowserManager.cpp | 124 ++++++++++++++++++++ src/MainWindow.cpp | 58 ++++++---- 6 files changed, 491 insertions(+), 26 deletions(-) create mode 100644 TODO.org create mode 100644 include/BrowserManager.hpp create mode 100644 spec/BrowserManagerSpec.cpp create mode 100644 src/BrowserManager.cpp diff --git a/TODO.org b/TODO.org new file mode 100644 index 0000000..d101052 --- /dev/null +++ b/TODO.org @@ -0,0 +1,19 @@ +** Current +- [X] Multi tabs +- [X] Open in new tab/new window +- [ ] Lua for command input +- [ ] Modal keys +- [ ] Tab history navigation + +** Next +- [ ] History persistance +- [ ] Sketch lua api for managing ui +- [ ] Simpler shell-ey/vimscript-ey language for command input + +** Later +- [ ] Fullscreen +- [ ] File picker +- [ ] Dev tools +- [ ] remote debugging with cdp (spider-repl) + +** Maybe diff --git a/include/BrowserManager.hpp b/include/BrowserManager.hpp new file mode 100644 index 0000000..84d6f6a --- /dev/null +++ b/include/BrowserManager.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +class BrowserManager : public QWidget { + Q_OBJECT + +public: + inline static const QUrl NewtabURL = QUrl("about:blank"); + +public: + BrowserManager(QWebEngineProfile *profile = new QWebEngineProfile); + + QUrl currentUrl(); + void setCurrentUrl(QUrl url); + + QWebEngineView *createNewWebView(QUrl url = BrowserManager::NewtabURL, + bool focus = false); + + std::vector webViewUrls(); + u_int32_t currentWebViewIndex(); + u_int32_t webViewCount(); + void focusWebView(long index); + void nextWebView(); + void previousWebView(); + + void closeWebView(long index); + void closeCurrentWebView(); + + void onNewWebViewRequest(QWebEngineNewWindowRequest &request); + +private: + QWebEngineProfile *profile; + QStackedLayout *layout; + QList webViewList; +}; diff --git a/include/MainWindow.hpp b/include/MainWindow.hpp index 552aa25..fde3178 100644 --- a/include/MainWindow.hpp +++ b/include/MainWindow.hpp @@ -4,6 +4,7 @@ #include #include +#include "BrowserManager.hpp" #include "CommandInput.hpp" class MainWindow : public QMainWindow { @@ -14,9 +15,10 @@ protected: void keyPressEvent(QKeyEvent *event) override; void toggleURLInput(); void evaluateCommand(QString command); - void hideInput(); + void hideURLInput(); + void showURLInput(); private: - QWebEngineView *web; + BrowserManager *browserManager; CommandInput *commandInput; }; diff --git a/spec/BrowserManagerSpec.cpp b/spec/BrowserManagerSpec.cpp new file mode 100644 index 0000000..f4af481 --- /dev/null +++ b/spec/BrowserManagerSpec.cpp @@ -0,0 +1,271 @@ +#include +#include +#include + +#include "BrowserManager.hpp" +#include "TestUtils.h" + +class BrowserManagerSpec : public QObject { + Q_OBJECT + + class FakeNewWindowRequest : public QWebEngineNewWindowRequest { + public: + FakeNewWindowRequest(DestinationType t, const QRect &r, const QUrl &u, + bool b) + : QWebEngineNewWindowRequest(t, r, u, b, nullptr) {} + }; + +private slots: + void testInitialState() { + context("when initialized"); + it("opens a single tab") { + BrowserManager browserManager; + + QCOMPARE(browserManager.webViewCount(), 1); + QCOMPARE(browserManager.currentUrl(), BrowserManager::NewtabURL); + browserManager.deleteLater(); + } + } + + void testCreateNewWebView() { + context("when creating a new background webview without a url"); + it("opens new tab url as a background view") { + BrowserManager browserManager; + + browserManager.createNewWebView(); + + QCOMPARE(browserManager.webViewCount(), 2); + std::vector urls = {QUrl(BrowserManager::NewtabURL), + QUrl(BrowserManager::NewtabURL)}; + QCOMPARE(browserManager.webViewUrls(), urls); + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), BrowserManager::NewtabURL); + } + + context("when creating a new webview with a url and focus is false"); + it("opens the given url in background") { + BrowserManager browserManager; + + browserManager.createNewWebView(QUrl("https://duckduckgo.com"), false); + + QCOMPARE(browserManager.webViewCount(), 2); + std::vector urls = {QUrl(BrowserManager::NewtabURL), + QUrl("https://duckduckgo.com")}; + QCOMPARE(browserManager.webViewUrls(), urls); + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), BrowserManager::NewtabURL); + } + + context("when creating a new webview with a url and focus is true"); + it("opens the given url as current view") { + BrowserManager browserManager; + + browserManager.createNewWebView(QUrl("https://duckduckgo.com"), true); + + QCOMPARE(browserManager.webViewCount(), 2); + std::vector urls = {QUrl(BrowserManager::NewtabURL), + QUrl("https://duckduckgo.com")}; + QCOMPARE(browserManager.webViewUrls(), urls); + QCOMPARE(browserManager.currentWebViewIndex(), 1); + QCOMPARE(browserManager.currentUrl(), QUrl("https://duckduckgo.com")); + } + } + + void testNextNavigation() { + context("when nextWebView is called"); + context("- and there is only 1 tab"); + it("does nothing") { + BrowserManager browserManager; + + browserManager.nextWebView(); + + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), BrowserManager::NewtabURL); + } + + context("when nextWebView is called"); + context("- and there are tabs after the current tab"); + it("goes to the next tab") { + BrowserManager browserManager; + browserManager.createNewWebView(QUrl("http://a1.com")); + browserManager.createNewWebView(QUrl("http://a2.com")); + + browserManager.nextWebView(); + + QCOMPARE(browserManager.currentWebViewIndex(), 1); + QCOMPARE(browserManager.currentUrl(), QUrl("http://a1.com")); + } + + context("when nextWebView is called"); + context("- and current tab is the last tab"); + it("jumps to the first tab") { + BrowserManager browserManager; + browserManager.createNewWebView(QUrl("http://a1.com")); + browserManager.createNewWebView(QUrl("http://a2.com"), true); + QCOMPARE(browserManager.currentWebViewIndex(), 2); + + browserManager.nextWebView(); + + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), BrowserManager::NewtabURL); + } + } + + void testPreviousNavigation() { + context("when previousWebView is called"); + context("- and there is only 1 tab"); + it("does nothing") { + BrowserManager browserManager; + + browserManager.previousWebView(); + + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), BrowserManager::NewtabURL); + } + + context("when previousWebView is called"); + context("- and there are tabs before the current tab"); + it("goes to the next tab") { + BrowserManager browserManager; + browserManager.createNewWebView(QUrl("http://a1.com")); + browserManager.createNewWebView(QUrl("http://a2.com"), true); + QCOMPARE(browserManager.currentWebViewIndex(), 2); + + browserManager.previousWebView(); + + QCOMPARE(browserManager.currentWebViewIndex(), 1); + QCOMPARE(browserManager.currentUrl(), QUrl("http://a1.com")); + } + + context("when previousWebView is called"); + context("- and current tab is the last tab"); + it("jumps to the last tab") { + BrowserManager browserManager; + browserManager.createNewWebView(QUrl("http://a1.com")); + browserManager.createNewWebView(QUrl("http://a2.com")); + + browserManager.previousWebView(); + + QCOMPARE(browserManager.currentWebViewIndex(), 2); + QCOMPARE(browserManager.currentUrl(), QUrl("http://a2.com")); + } + } + + void testCloseWebView() { + context("when closeWebView is called"); + context("- with out of bounds index"); + it("does nothing") { + BrowserManager browserManager; + browserManager.setCurrentUrl(QUrl("https://a.com")); + + browserManager.closeWebView(1); + + QCOMPARE(browserManager.webViewCount(), 1); + QCOMPARE(browserManager.webViewUrls(), + (std::vector{QUrl("https://a.com")})); + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), QUrl("https://a.com")); + } + + context("when closeWebView is called"); + context("- and there is only 1 tab"); + it("closes the tab and opens empty tab in its place") { + BrowserManager browserManager; + browserManager.setCurrentUrl(QUrl("https://a.com")); + QCOMPARE(browserManager.webViewCount(), 1); + + browserManager.closeWebView(0); + + QCOMPARE(browserManager.webViewCount(), 1); + QCOMPARE(browserManager.webViewUrls(), + (std::vector{BrowserManager::NewtabURL})); + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), BrowserManager::NewtabURL); + } + + context("when closeWebView is called"); + context("- with the current tab index"); + context("- and there are some tabs after"); + it("closes the tab and focuses the next tab") { + BrowserManager browserManager; + browserManager.createNewWebView(QUrl("https://a1.com"), true); + browserManager.createNewWebView(QUrl("https://a2.com")); + QCOMPARE(browserManager.webViewCount(), 3); + QCOMPARE(browserManager.currentWebViewIndex(), 1); + + browserManager.closeWebView(1); + + QCOMPARE(browserManager.webViewCount(), 2); + QCOMPARE(browserManager.webViewUrls(), + (std::vector{BrowserManager::NewtabURL, + QUrl("https://a2.com")})); + QCOMPARE(browserManager.currentWebViewIndex(), 1); + QCOMPARE(browserManager.currentUrl(), QUrl("https://a2.com")); + } + + context("when closeWebView is called"); + context("- with the current tab index"); + context("- which is the last tab"); + it("closes the tab and focusses previous tab") { + BrowserManager browserManager; + browserManager.createNewWebView(QUrl("https://a1.com")); + browserManager.createNewWebView(QUrl("https://a2.com"), true); + QCOMPARE(browserManager.webViewCount(), 3); + QCOMPARE(browserManager.currentWebViewIndex(), 2); + + browserManager.closeWebView(2); + + QCOMPARE(browserManager.webViewCount(), 2); + QCOMPARE(browserManager.webViewUrls(), + (std::vector{BrowserManager::NewtabURL, + QUrl("https://a1.com")})); + QCOMPARE(browserManager.currentWebViewIndex(), 1); + QCOMPARE(browserManager.currentUrl(), QUrl("https://a1.com")); + } + } + + void testNewWindowRequestSignal() { + context("when webview emits a newWindowRequested signal"); + context("- of type new tab"); + it("opens a new web view and focusses it") { + BrowserManager browserManager; + browserManager.setCurrentUrl(QUrl("https://a.com")); + auto webview = browserManager.findChild(); + + FakeNewWindowRequest windowRequest( + FakeNewWindowRequest::DestinationType::InNewTab, QRect(0, 0, 0, 0), + QUrl("https://new.com"), true); + emit webview->page()->newWindowRequested(windowRequest); + + QCOMPARE(browserManager.webViewCount(), 2); + QCOMPARE( + browserManager.webViewUrls(), + (std::vector{QUrl("https://a.com"), QUrl("https://new.com")})); + QCOMPARE(browserManager.currentWebViewIndex(), 1); + QCOMPARE(browserManager.currentUrl(), QUrl("https://new.com")); + } + + context("when webview emits a newWindowRequested signal"); + context("- of type new background tab"); + it("opens a new web view in the background") { + BrowserManager browserManager; + browserManager.setCurrentUrl(QUrl("https://a.com")); + auto webview = browserManager.findChild(); + + FakeNewWindowRequest windowRequest( + FakeNewWindowRequest::DestinationType::InNewBackgroundTab, + QRect(0, 0, 0, 0), QUrl("https://new.com"), true); + emit webview->page()->newWindowRequested(windowRequest); + + QCOMPARE(browserManager.webViewCount(), 2); + QCOMPARE( + browserManager.webViewUrls(), + (std::vector{QUrl("https://a.com"), QUrl("https://new.com")})); + QCOMPARE(browserManager.currentWebViewIndex(), 0); + QCOMPARE(browserManager.currentUrl(), QUrl("https://a.com")); + } + } +}; + +QTEST_REGISTER(BrowserManagerSpec) +#include "BrowserManagerSpec.moc" diff --git a/src/BrowserManager.cpp b/src/BrowserManager.cpp new file mode 100644 index 0000000..ddc6105 --- /dev/null +++ b/src/BrowserManager.cpp @@ -0,0 +1,124 @@ +#include +#include +#include + +#include "BrowserManager.hpp" + +BrowserManager::BrowserManager(QWebEngineProfile *profile) : QWidget() { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + layout = new QStackedLayout(this); + layout->setStackingMode(QStackedLayout::StackAll); + layout->setContentsMargins(0, 0, 0, 0); + + this->profile = profile; + + createNewWebView(BrowserManager::NewtabURL, true); +} + +QWebEngineView *BrowserManager::createNewWebView(QUrl url, bool focus) { + auto webview = new QWebEngineView(profile); + webview->setUrl(url); + layout->addWidget(webview); + webViewList.append(webview); + + connect(webview->page(), &QWebEnginePage::newWindowRequested, this, + &BrowserManager::onNewWebViewRequest); + + if (focus) + focusWebView(webViewList.length() - 1); + + return webview; +} + +void BrowserManager::onNewWebViewRequest(QWebEngineNewWindowRequest &request) { + switch (request.destination()) { + case QWebEngineNewWindowRequest::InNewTab: + createNewWebView(request.requestedUrl(), true); + break; + case QWebEngineNewWindowRequest::InNewBackgroundTab: + createNewWebView(request.requestedUrl(), false); + break; + case QWebEngineNewWindowRequest::InNewWindow: + // TODO: Impl + createNewWebView(request.requestedUrl(), true); + break; + case QWebEngineNewWindowRequest::InNewDialog: + // TODO: Impl + createNewWebView(request.requestedUrl(), true); + break; + } +} + +void BrowserManager::nextWebView() { + if (webViewList.isEmpty()) + return; + auto index = currentWebViewIndex() + 1; + auto total = webViewList.length(); + index = index >= total ? index % total : index; + focusWebView(index); +} + +void BrowserManager::previousWebView() { + if (webViewList.isEmpty()) + return; + auto index = currentWebViewIndex() - 1; + auto total = webViewList.length(); + index = index < 0 ? total + index : index; + focusWebView(index); +} + +void BrowserManager::closeCurrentWebView() { + closeWebView(currentWebViewIndex()); +} + +void BrowserManager::closeWebView(long index) { + if (index < 0 || index >= webViewList.length()) + return; + + auto webview = webViewList.at(index); + layout->removeWidget(webview); + webViewList.removeAt(index); + disconnect(webview->page()); + webview->deleteLater(); + + focusWebView(currentWebViewIndex()); + + if (webViewList.isEmpty()) { + createNewWebView(BrowserManager::NewtabURL, true); + } +} + +std::vector BrowserManager::webViewUrls() { + std::vector urls; + for (auto &view : webViewList) + urls.push_back(view->url()); + return urls; +} + +u_int32_t BrowserManager::currentWebViewIndex() { + return layout->currentIndex(); +} +u_int32_t BrowserManager::webViewCount() { return webViewList.length(); } + +void BrowserManager::focusWebView(long index) { + if (webViewList.isEmpty()) + return; + + index = std::max((long)0, std::min(index, (long)webViewList.length() - 1)); + layout->setCurrentIndex(index); +} + +QUrl BrowserManager::currentUrl() { + if (currentWebViewIndex() >= webViewList.length()) + return QUrl("about:newtab"); + + return webViewList.at(currentWebViewIndex())->url(); +} + +void BrowserManager::setCurrentUrl(QUrl url) { + if (currentWebViewIndex() >= webViewList.length()) + return; + + webViewList.at(currentWebViewIndex())->setUrl(url); +} diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 809e5de..ee8d128 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2,49 +2,53 @@ #include #include #include -#include -#include +#include "BrowserManager.hpp" #include "CommandInput.hpp" #include "MainWindow.hpp" MainWindow::MainWindow() { setStyleSheet("background-color: #000; color: #fff;"); + setCentralWidget(new QWidget()); - auto centralWidget = new QWidget(); - setCentralWidget(centralWidget); - + // Root stacked layout auto layout = new QStackedLayout(); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - centralWidget->setLayout(layout); layout->setStackingMode(QStackedLayout::StackAll); + centralWidget()->setLayout(layout); - // Webengine - web = new QWebEngineView(); - web->setUrl(QUrl("https://ediblemonad.dev")); - layout->addWidget(web); + // Web engine + browserManager = new BrowserManager(new QWebEngineProfile("web-browser")); + layout->addWidget(browserManager); // Command input - commandInput = new CommandInput(web->url().toString()); - hideInput(); + commandInput = new CommandInput(browserManager->currentUrl().toString()); + hideURLInput(); commandInput->move(0, 0); connect(commandInput, &CommandInput::submitted, this, &MainWindow::evaluateCommand); - connect(commandInput, &CommandInput::cancelled, this, &MainWindow::hideInput); - + connect(commandInput, &CommandInput::cancelled, this, + &MainWindow::hideURLInput); layout->addWidget(commandInput); } -void MainWindow::hideInput() { +void MainWindow::hideURLInput() { commandInput->setInputFocus(false); commandInput->setHidden(true); } +void MainWindow::showURLInput() { + commandInput->setInputText(browserManager->currentUrl().toString()); + commandInput->raise(); + commandInput->setHidden(false); + commandInput->setInputFocus(true); +} + void MainWindow::evaluateCommand(QString command) { if (!command.isEmpty()) - web->setUrl(command); - hideInput(); + browserManager->setCurrentUrl(command); + hideURLInput(); } void MainWindow::keyPressEvent(QKeyEvent *event) { @@ -52,15 +56,21 @@ void MainWindow::keyPressEvent(QKeyEvent *event) { if (combo.key() == Qt::Key_L && combo.keyboardModifiers().testFlag(Qt::ControlModifier)) { toggleURLInput(); + } else if (combo.key() == Qt::Key_T && + combo.keyboardModifiers().testFlag(Qt::ControlModifier)) { + browserManager->createNewWebView(QUrl("https://duckduckgo.com"), true); + } else if (combo.key() == Qt::Key_J && + combo.keyboardModifiers().testFlag(Qt::ControlModifier)) { + browserManager->nextWebView(); + } else if (combo.key() == Qt::Key_K && + combo.keyboardModifiers().testFlag(Qt::ControlModifier)) { + browserManager->previousWebView(); + } else if (combo.key() == Qt::Key_W && + combo.keyboardModifiers().testFlag(Qt::ControlModifier)) { + browserManager->closeCurrentWebView(); } } void MainWindow::toggleURLInput() { - auto shouldShow = commandInput->isHidden(); - if (shouldShow) { - commandInput->setInputText(web->url().toString()); - commandInput->raise(); - } - commandInput->setHidden(!shouldShow); - commandInput->setInputFocus(shouldShow); + commandInput->isHidden() ? showURLInput() : hideURLInput(); } -- cgit v1.3.1