Skip to content

Commit ff4a84b

Browse files
feat(browser): add Quick Browse with isolated WebView2 profiles
Quick Browse was missing as a first-class entry point for ad-hoc web navigation. Users had no way to open an arbitrary URL in an embedded WebView2 without defining a full MiniApp first. - RAM warning now counts quick-browser tabs alongside mini-app instances so the threshold reflects true WebView2 process cost - WebViewWidget::create gains a userDataFolder override so each Quick Browse session gets an isolated AppData profile; stale profile directories are swept on startup via sweepStaleQuickBrowserData() - menuMiniApps renamed to menuBrowser with a menuMiniAppsSub submenu, keeping mini-app actions one level deeper and freeing the top-level Browser menu for Quick Browse - SDDL-based ACL replaced with SetEntriesInAclW/ SetNamedSecurityInfoW (aclapi.h) for reliable inheritance on the user-data directory; nukeDirectory() added for profile cleanup - WebView2 stub expanded to the full ICoreWebView2 vtable (58 methods, SDK 1.0.2903.40) and all handler forward declarations needed by the Windows backend - titleChanged signal added to WebViewWidget so dock tab titles can track page title updates
1 parent d6cdb68 commit ff4a84b

9 files changed

Lines changed: 548 additions & 47 deletions

File tree

src/MiniAppManager.cpp

Lines changed: 175 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@
1818
#include <QClipboard>
1919
#include <QDesktopServices>
2020
#include <QDialog>
21+
#include <QDir>
2122
#include <QFont>
23+
#include <QHostAddress>
2224
#include <QMenu>
2325
#include <QMessageBox>
2426
#include <QPainter>
2527
#include <QPlainTextEdit>
2628
#include <QProcess>
29+
#include <QSet>
30+
#include <QStandardPaths>
31+
#include <QTcpSocket>
32+
#include <QTimer>
2733
#include <QUrl>
34+
#include <QUuid>
2835
#include <QVBoxLayout>
2936

3037
#include <DockWidget.h>
@@ -57,6 +64,8 @@ MiniAppManager::MiniAppManager(NotepadNextApplication *app,
5764
, m_dockedEditor(dockedEditor)
5865
, m_iconPath(QStringLiteral(":/icons/mini-app.svg"))
5966
{
67+
sweepStaleQuickBrowserData();
68+
6069
// Re-tint tab icons on theme change
6170
connect(app, &NotepadNextApplication::effectiveThemeChanged, this, &MiniAppManager::retintAllIcons);
6271
}
@@ -77,13 +86,14 @@ void MiniAppManager::launchApp(const MiniAppDefinition &def)
7786
}
7887
}
7988

80-
// Warning at 4th instance (3 already running)
81-
if (m_instances.size() >= 3) {
89+
// Warning at 4th instance (3 already running, counting quick browser tabs)
90+
const int totalWebViews = m_instances.size() + m_quickBrowserTabs.size();
91+
if (totalWebViews >= 3) {
8292
QMessageBox::StandardButton btn = QMessageBox::warning(
8393
nullptr,
8494
tr("Mini Apps"),
85-
tr("Each Mini App uses ~100MB RAM. You have %1 running. Continue?")
86-
.arg(m_instances.size()),
95+
tr("Each Mini App uses ~100MB RAM. You have %1 WebView2 instances running. Continue?")
96+
.arg(totalWebViews),
8797
QMessageBox::Yes | QMessageBox::Cancel,
8898
QMessageBox::Cancel);
8999
if (btn != QMessageBox::Yes)
@@ -199,6 +209,13 @@ void MiniAppManager::onInstanceFinished(MiniAppInstance *instance)
199209

200210
void MiniAppManager::shutdown()
201211
{
212+
// Destroy quick browser webviews
213+
for (const QuickBrowserTab &tab : m_quickBrowserTabs) {
214+
if (tab.webView)
215+
tab.webView->destroy();
216+
}
217+
m_quickBrowserTabs.clear();
218+
202219
for (MiniAppInstance *inst : m_instances) {
203220
inst->destroy();
204221
}
@@ -210,13 +227,167 @@ void MiniAppManager::shutdown()
210227
m_instances.clear();
211228
}
212229

230+
void MiniAppManager::sweepStaleQuickBrowserData()
231+
{
232+
const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
233+
+ QStringLiteral("/QuickBrowser");
234+
QDir baseDir(basePath);
235+
if (!baseDir.exists())
236+
return;
237+
238+
const QStringList entries = baseDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
239+
for (const QString &entry : entries) {
240+
QDir subDir(baseDir.filePath(entry));
241+
if (!subDir.removeRecursively()) {
242+
qWarning("MiniAppManager: failed to sweep stale QuickBrowser data: %s",
243+
qUtf8Printable(subDir.path()));
244+
}
245+
}
246+
}
247+
248+
void MiniAppManager::launchQuickBrowser(const QUrl &url, bool enableCdp)
249+
{
250+
#ifdef Q_OS_LINUX
251+
QDesktopServices::openUrl(url);
252+
return;
253+
#endif
254+
255+
// RAM warning at 4th total WebView2 instance
256+
const int totalWebViews = m_instances.size() + m_quickBrowserTabs.size();
257+
if (totalWebViews >= 3) {
258+
QMessageBox::StandardButton btn = QMessageBox::warning(
259+
nullptr,
260+
tr("Quick Browser"),
261+
tr("Each browser tab uses ~100MB RAM. You have %1 WebView2 instances running. Continue?")
262+
.arg(totalWebViews),
263+
QMessageBox::Yes | QMessageBox::Cancel,
264+
QMessageBox::Cancel);
265+
if (btn != QMessageBox::Yes)
266+
return;
267+
}
268+
269+
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
270+
const QString appId = QStringLiteral("qb-") + uuid;
271+
const QString userDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
272+
+ QStringLiteral("/QuickBrowser/") + uuid;
273+
274+
int debugPort = 0;
275+
if (enableCdp) {
276+
QSet<int> reserved;
277+
for (const MiniAppDefinition &def : m_registry->globalApps()) {
278+
if (def.debugPort > 0)
279+
reserved.insert(def.debugPort);
280+
}
281+
for (const MiniAppInstance *inst : m_instances) {
282+
if (inst->definition().debugPort > 0)
283+
reserved.insert(inst->definition().debugPort);
284+
}
285+
QTcpSocket sock;
286+
for (int port = 9222; port <= 9322; ++port) {
287+
if (reserved.contains(port))
288+
continue;
289+
if (sock.bind(QHostAddress::LocalHost, port)) {
290+
debugPort = port;
291+
sock.close();
292+
break;
293+
}
294+
}
295+
}
296+
297+
auto *webView = WebViewWidget::create(appId, url, debugPort, nullptr, userDataPath);
298+
if (!webView)
299+
return;
300+
301+
ads::CDockWidget *dw = m_dockedEditor->addPreviewTab(
302+
webView, url.host().isEmpty() ? url.toString() : url.host(), tintedGlobeIcon());
303+
304+
QuickBrowserTab tab;
305+
tab.webView = webView;
306+
tab.dockWidget = dw;
307+
tab.userDataPath = userDataPath;
308+
m_quickBrowserTabs.append(tab);
309+
310+
// Wire tab close → cleanup
311+
connect(dw, &ads::CDockWidget::closed, this, [this, webView, dw, userDataPath]() {
312+
webView->destroy();
313+
314+
// Remove from tracked list
315+
for (int i = 0; i < m_quickBrowserTabs.size(); ++i) {
316+
if (m_quickBrowserTabs[i].dockWidget == dw) {
317+
m_quickBrowserTabs.removeAt(i);
318+
break;
319+
}
320+
}
321+
322+
// Retry-delete user data folder
323+
auto *timer = new QTimer(this);
324+
timer->setInterval(1000);
325+
int *attempts = new int(0);
326+
connect(timer, &QTimer::timeout, this, [timer, attempts, userDataPath]() {
327+
++(*attempts);
328+
QDir dir(userDataPath);
329+
if (!dir.exists() || dir.removeRecursively()) {
330+
timer->stop();
331+
delete attempts;
332+
timer->deleteLater();
333+
return;
334+
}
335+
if (*attempts >= 5) {
336+
qWarning("MiniAppManager: failed to delete QuickBrowser data after 5 attempts: %s",
337+
qUtf8Printable(userDataPath));
338+
timer->stop();
339+
delete attempts;
340+
timer->deleteLater();
341+
}
342+
});
343+
timer->start();
344+
});
345+
346+
// Wire title changes → tab title update
347+
connect(webView, &WebViewWidget::titleChanged, dw, [dw](const QString &title) {
348+
if (!title.isEmpty())
349+
dw->setWindowTitle(title);
350+
});
351+
352+
dw->tabWidget()->setContextMenuPolicy(Qt::CustomContextMenu);
353+
connect(dw->tabWidget(), &QWidget::customContextMenuRequested, this, [this, webView, dw](const QPoint &pos) {
354+
QMenu menu;
355+
auto *mainWin = qobject_cast<MainWindow *>(parent());
356+
AiAgentDock *aiDock = mainWin ? mainWin->activeAiDock() : nullptr;
357+
if (aiDock && !webView->cdpHttpUrl().isEmpty()) {
358+
menu.addAction(tr("Send to AI"), this, [webView, aiDock]() {
359+
const QString msg = QStringLiteral(
360+
"{{ Connect to the browser via CDP at %1 "
361+
"(list available pages first, then interact only with the existing page — do not create new pages). }}\n\n")
362+
.arg(webView->cdpHttpUrl());
363+
aiDock->insertTextToInput(msg);
364+
aiDock->setVisible(true);
365+
aiDock->raise();
366+
});
367+
}
368+
QAction *cdpAction = menu.addAction(tr("Copy CDP URL"), this, [webView]() {
369+
QApplication::clipboard()->setText(webView->cdpHttpUrl());
370+
});
371+
cdpAction->setEnabled(!webView->cdpHttpUrl().isEmpty());
372+
menu.addSeparator();
373+
menu.addAction(tr("Close"), dw, &ads::CDockWidget::closeDockWidget);
374+
menu.exec(dw->tabWidget()->mapToGlobal(pos));
375+
});
376+
377+
webView->initialize();
378+
}
379+
213380
void MiniAppManager::retintAllIcons()
214381
{
215382
QIcon icon = tintedGlobeIcon();
216383
for (MiniAppInstance *inst : m_instances) {
217384
if (inst->dockWidget())
218385
inst->dockWidget()->tabWidget()->setIcon(icon);
219386
}
387+
for (const QuickBrowserTab &tab : m_quickBrowserTabs) {
388+
if (tab.dockWidget)
389+
tab.dockWidget->tabWidget()->setIcon(icon);
390+
}
220391
}
221392

222393
QIcon MiniAppManager::tintedGlobeIcon() const

src/MiniAppManager.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,22 @@
1212
#include <QIcon>
1313
#include <QList>
1414
#include <QObject>
15+
#include <QUrl>
1516

1617
class DockedEditor;
1718
class MiniAppInstance;
1819
class MiniAppRegistry;
1920
class NotepadNextApplication;
21+
class WebViewWidget;
2022

2123
namespace ads { class CDockWidget; }
2224

25+
struct QuickBrowserTab {
26+
WebViewWidget *webView = nullptr;
27+
ads::CDockWidget *dockWidget = nullptr;
28+
QString userDataPath;
29+
};
30+
2331
class MiniAppManager : public QObject
2432
{
2533
Q_OBJECT
@@ -32,6 +40,7 @@ class MiniAppManager : public QObject
3240
~MiniAppManager() override;
3341

3442
void launchApp(const MiniAppDefinition &def);
43+
void launchQuickBrowser(const QUrl &url, bool enableCdp = true);
3544
void shutdown();
3645

3746
int runningCount() const { return m_instances.size(); }
@@ -44,10 +53,12 @@ class MiniAppManager : public QObject
4453
void onInstanceFinished(MiniAppInstance *instance);
4554
void retintAllIcons();
4655
QIcon tintedGlobeIcon() const;
56+
void sweepStaleQuickBrowserData();
4757

4858
NotepadNextApplication *m_app;
4959
MiniAppRegistry *m_registry;
5060
DockedEditor *m_dockedEditor;
5161
QList<MiniAppInstance *> m_instances;
62+
QList<QuickBrowserTab> m_quickBrowserTabs;
5263
QString m_iconPath;
5364
};

src/dialogs/MainWindow.cpp

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
#include <QDialogButtonBox>
4141
#include <QDir>
4242
#include <QLineEdit>
43+
#include <QLabel>
44+
#include <QCheckBox>
45+
#include <QVBoxLayout>
4346
#include <QPrintPreviewDialog>
4447
#include <QPrinter>
4548
#include <QDirIterator>
@@ -1391,10 +1394,10 @@ MainWindow::MainWindow(NotepadNextApplication *app) :
13911394
m_miniAppRegistry = new MiniAppRegistry(app->getSettings());
13921395
m_miniAppManager = new MiniAppManager(app, m_miniAppRegistry, dockedEditor, this);
13931396

1394-
connect(ui->menuMiniApps, &QMenu::aboutToShow, this, [this]() {
1397+
connect(ui->menuMiniAppsSub, &QMenu::aboutToShow, this, [this]() {
13951398
// Clear dynamic items (keep only the static "Edit Mini Apps..." action)
1396-
while (ui->menuMiniApps->actions().size() > 1) {
1397-
delete ui->menuMiniApps->actions().first();
1399+
while (ui->menuMiniAppsSub->actions().size() > 1) {
1400+
delete ui->menuMiniAppsSub->actions().first();
13981401
}
13991402

14001403
const QString workspaceRoot = currentWorkspaceRoot();
@@ -1407,29 +1410,60 @@ MainWindow::MainWindow(NotepadNextApplication *app) :
14071410

14081411
// Global apps
14091412
for (const MiniAppDefinition &def : globalApps) {
1410-
QAction *a = new QAction(def.name, ui->menuMiniApps);
1413+
QAction *a = new QAction(def.name, ui->menuMiniAppsSub);
14111414
connect(a, &QAction::triggered, this, [this, def]() {
14121415
m_miniAppManager->launchApp(def);
14131416
});
1414-
ui->menuMiniApps->insertAction(beforeAction, a);
1417+
ui->menuMiniAppsSub->insertAction(beforeAction, a);
14151418
}
14161419

14171420
// Separator + workspace apps
14181421
if (!wsApps.isEmpty()) {
14191422
if (!globalApps.isEmpty())
1420-
ui->menuMiniApps->insertSeparator(beforeAction);
1423+
ui->menuMiniAppsSub->insertSeparator(beforeAction);
14211424
for (const MiniAppDefinition &def : wsApps) {
1422-
QAction *a = new QAction(def.name, ui->menuMiniApps);
1425+
QAction *a = new QAction(def.name, ui->menuMiniAppsSub);
14231426
connect(a, &QAction::triggered, this, [this, def]() {
14241427
m_miniAppManager->launchApp(def);
14251428
});
1426-
ui->menuMiniApps->insertAction(beforeAction, a);
1429+
ui->menuMiniAppsSub->insertAction(beforeAction, a);
14271430
}
14281431
}
14291432

14301433
// Separator before Edit action (if any apps exist)
14311434
if (!globalApps.isEmpty() || !wsApps.isEmpty())
1432-
ui->menuMiniApps->insertSeparator(beforeAction);
1435+
ui->menuMiniAppsSub->insertSeparator(beforeAction);
1436+
});
1437+
1438+
// Quick Browse action
1439+
ui->actionQuickBrowser->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_B));
1440+
connect(ui->actionQuickBrowser, &QAction::triggered, this, [this]() {
1441+
QDialog dlg(this);
1442+
dlg.setWindowTitle(tr("Quick Browse"));
1443+
auto *layout = new QVBoxLayout(&dlg);
1444+
layout->addWidget(new QLabel(tr("Enter URL:"), &dlg));
1445+
auto *urlEdit = new QLineEdit(&dlg);
1446+
urlEdit->setPlaceholderText(QStringLiteral("https://example.com"));
1447+
layout->addWidget(urlEdit);
1448+
auto *cdpCheck = new QCheckBox(tr("Enable CDP debugging"), &dlg);
1449+
cdpCheck->setChecked(true);
1450+
layout->addWidget(cdpCheck);
1451+
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
1452+
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
1453+
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
1454+
layout->addWidget(buttons);
1455+
urlEdit->setFocus();
1456+
if (dlg.exec() != QDialog::Accepted)
1457+
return;
1458+
QString input = urlEdit->text().trimmed();
1459+
if (input.isEmpty())
1460+
return;
1461+
if (!input.contains(QStringLiteral("://")))
1462+
input = QStringLiteral("https://") + input;
1463+
QUrl url(input, QUrl::TolerantMode);
1464+
if (!url.isValid())
1465+
return;
1466+
m_miniAppManager->launchQuickBrowser(url, cdpCheck->isChecked());
14331467
});
14341468

14351469
connect(ui->actionEditMiniApps, &QAction::triggered, this, [this]() {

0 commit comments

Comments
 (0)