plyght's own C++ browser for macOS
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

up

plyght 27275b77 43d9a4f5

+614 -49
+4 -1
CMakeLists.txt
··· 1 1 cmake_minimum_required(VERSION 3.24) 2 2 3 - project(pocb VERSION 0.1.0 LANGUAGES CXX) 3 + project(pocb VERSION 0.1.0 LANGUAGES CXX OBJCXX) 4 4 5 5 set(CMAKE_CXX_STANDARD 20) 6 6 set(CMAKE_CXX_STANDARD_REQUIRED ON) ··· 22 22 src/services/Theme.hpp 23 23 src/services/FaviconService.cpp 24 24 src/services/FaviconService.hpp 25 + src/services/ChromeExtensionManager.cpp 26 + src/services/ChromeExtensionManager.hpp 25 27 src/ui/FloatingOmnibox.cpp 26 28 src/ui/FloatingOmnibox.hpp 27 29 src/ui/AddressBarController.cpp ··· 65 67 foreach(_src IN LISTS POCB_OBJC_SOURCES) 66 68 set_source_files_properties(${_src} PROPERTIES COMPILE_FLAGS "-fobjc-arc") 67 69 endforeach() 70 + set_source_files_properties(src/services/ChromeExtensionManager.cpp PROPERTIES LANGUAGE OBJCXX COMPILE_FLAGS "-fobjc-arc") 68 71 target_link_libraries(pocb PRIVATE 69 72 "-framework AppKit" 70 73 "-framework WebKit"
+24
src/app/BrowserWindow.cpp
··· 4 4 #include "AddressBarController.hpp" 5 5 #include "FloatingOmnibox.hpp" 6 6 #include "ChromeWidgets.hpp" 7 + #include "ChromeExtensionManager.hpp" 7 8 #include "LayoutMetrics.hpp" 8 9 #include "MacIntegration.hpp" 9 10 #include "SettingsDialog.hpp" ··· 71 72 #endif 72 73 setupUi(); 73 74 setupActions(); 75 + ChromeExtensionManager::setBrowserWindow(this); 74 76 setWindowTitle("pocb"); 75 77 { 76 78 QSettings settings; ··· 125 127 mac::applyVibrancyBehind(host, mac::VibrancyMaterial::Sidebar); 126 128 } 127 129 }); 130 + } 131 + 132 + WebView *BrowserWindow::extensionCurrentView() const { 133 + return currentView(); 134 + } 135 + 136 + QList<WebView *> BrowserWindow::extensionViews() const { 137 + return m_tabTree ? m_tabTree->views() : QList<WebView *>(); 138 + } 139 + 140 + WebView *BrowserWindow::extensionCreateTab(const QUrl &url, bool background) { 141 + return m_tabTree ? m_tabTree->newTabForExtension(url, background) : nullptr; 142 + } 143 + 144 + void BrowserWindow::extensionSelectView(WebView *view) { 145 + if (m_tabTree) m_tabTree->selectView(view); 146 + } 147 + 148 + void BrowserWindow::extensionCloseView(WebView *view) { 149 + if (!m_tabTree || !view) return; 150 + m_tabTree->selectView(view); 151 + if (m_tabTree->currentView() == view) m_tabTree->closeCurrent(); 128 152 } 129 153 130 154 void BrowserWindow::loadFromOmnibox() {
+6
src/app/BrowserWindow.hpp
··· 5 5 #include "Theme.hpp" 6 6 7 7 #include <QHash> 8 + #include <QList> 8 9 #include <QMainWindow> 9 10 #include <QUrl> 10 11 #include <functional> ··· 33 34 Q_OBJECT 34 35 public: 35 36 explicit BrowserWindow(QWidget *parent = nullptr); 37 + WebView *extensionCurrentView() const; 38 + QList<WebView *> extensionViews() const; 39 + WebView *extensionCreateTab(const QUrl &url, bool background); 40 + void extensionSelectView(WebView *view); 41 + void extensionCloseView(WebView *view); 36 42 37 43 protected: 38 44 void showEvent(QShowEvent *e) override;
+34
src/app/SettingsDialog.cpp
··· 1 1 #include "SettingsDialog.hpp" 2 2 3 3 #include "MacIntegration.hpp" 4 + #include "ChromeExtensionManager.hpp" 4 5 #include "ProfileStore.hpp" 5 6 #include "Theme.hpp" 6 7 7 8 #include <QCheckBox> 8 9 #include <QComboBox> 10 + #include <QFileDialog> 9 11 #include <QFormLayout> 10 12 #include <QFrame> 11 13 #include <QHBoxLayout> ··· 158 160 theme, browseCard)); 159 161 root->addWidget(browseCard); 160 162 163 + root->addWidget(makeSectionHeader("Extensions", theme, this)); 164 + auto *extensionsCard = makeCard(theme, this); 165 + auto *extensionsCol = new QVBoxLayout(extensionsCard); 166 + extensionsCol->setContentsMargins(16, 14, 16, 14); 167 + extensionsCol->setSpacing(10); 168 + 169 + auto *extensionRow = new QWidget(extensionsCard); 170 + auto *extensionLayout = new QHBoxLayout(extensionRow); 171 + extensionLayout->setContentsMargins(0, 0, 0, 0); 172 + extensionLayout->setSpacing(8); 173 + m_extensionPaths = new QLineEdit(ChromeExtensionManager::configuredPaths().join(";"), extensionRow); 174 + m_extensionPaths->setPlaceholderText("/path/to/unpacked-extension;/path/to/another-extension"); 175 + auto *addExtension = new QPushButton("Add folder", extensionRow); 176 + addExtension->setIcon(mac::sfSymbolIcon("folder", 12.0, theme.foreground)); 177 + addExtension->setIconSize(QSize(13, 13)); 178 + addExtension->setCursor(Qt::PointingHandCursor); 179 + extensionLayout->addWidget(m_extensionPaths, 1); 180 + extensionLayout->addWidget(addExtension); 181 + extensionsCol->addWidget(extensionRow); 182 + extensionsCol->addWidget(makeHelp( 183 + "Loads unpacked Chrome extension content scripts into new tabs. WebKit cannot run Chrome background workers, popups, or privileged Chrome APIs.", 184 + theme, extensionsCard)); 185 + root->addWidget(extensionsCard); 186 + 161 187 root->addStretch(1); 162 188 163 189 // ---- Footer buttons --------------------------------------------------- ··· 190 216 refreshProfiles(); 191 217 }); 192 218 connect(m_profileBox, &QComboBox::currentTextChanged, &m_profiles, &ProfileStore::setCurrentProfile); 219 + connect(addExtension, &QPushButton::clicked, this, [this] { 220 + const QString dir = QFileDialog::getExistingDirectory(this, "Choose unpacked Chrome extension"); 221 + if (dir.isEmpty()) return; 222 + QStringList paths = m_extensionPaths->text().split(';', Qt::SkipEmptyParts); 223 + if (!paths.contains(dir)) paths << dir; 224 + m_extensionPaths->setText(paths.join(';')); 225 + }); 193 226 connect(save, &QPushButton::clicked, this, [this] { 194 227 emit homePageChanged(m_homePage->text()); 195 228 emit searchEngineChanged(m_searchEngine->text()); ··· 197 230 QSettings().setValue("ui/showFullUrl", full); 198 231 emit showFullUrlChanged(full); 199 232 QSettings().setValue("ui/addressBarInSidebar", m_addrInSidebar->isChecked()); 233 + ChromeExtensionManager::setConfiguredPaths(m_extensionPaths->text().split(';', Qt::SkipEmptyParts)); 200 234 accept(); 201 235 }); 202 236 connect(cancel, &QPushButton::clicked, this, &QDialog::reject);
+1
src/app/SettingsDialog.hpp
··· 38 38 QLineEdit *m_newProfile = nullptr; 39 39 QLineEdit *m_homePage = nullptr; 40 40 QLineEdit *m_searchEngine = nullptr; 41 + QLineEdit *m_extensionPaths = nullptr; 41 42 QCheckBox *m_showFullUrl = nullptr; 42 43 QCheckBox *m_addrInSidebar = nullptr; 43 44 };
+6
src/mac/MacIntegration.hpp
··· 50 50 // NSWindow shadow is enabled so the shadow tracks the rounded silhouette. 51 51 void makeFloatingVibrantPanel(QWidget *window, VibrancyMaterial material, double cornerRadius); 52 52 53 + // Configure a top-level QWidget as a transparent rounded floating panel 54 + // without adding AppKit vibrancy. Qt's own translucent painting remains 55 + // visible, so content behind the popup shows through instead of the macOS 56 + // material tint. 57 + void makeTransparentFloatingPanel(QWidget *window, double cornerRadius); 58 + 53 59 // Make the NSWindow's titlebar transparent and hide its title text, so the 54 60 // behind-window vibrancy reads through the titlebar area as well. The 55 61 // content view is expanded to full size so Qt widgets can paint into the
+39 -5
src/mac/Vibrancy.mm
··· 23 23 NSVisualEffectView *v = [[NSVisualEffectView alloc] initWithFrame:frame]; 24 24 v.material = materialFor(m); 25 25 v.blendingMode = blendingMode; 26 - v.state = NSVisualEffectStateFollowsWindowActiveState; 26 + v.state = NSVisualEffectStateActive; 27 27 v.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; 28 28 return v; 29 29 } ··· 71 71 if (@available(macOS 10.15, *)) { 72 72 [content.layer setValue:@"continuous" forKey:@"cornerCurve"]; 73 73 } 74 - for (NSView *sub in content.subviews) { 74 + NSView *target = content.superview ?: content; 75 + target.wantsLayer = YES; 76 + target.layer.cornerRadius = cornerRadius; 77 + target.layer.masksToBounds = YES; 78 + target.layer.backgroundColor = NSColor.clearColor.CGColor; 79 + if (@available(macOS 10.15, *)) { 80 + [target.layer setValue:@"continuous" forKey:@"cornerCurve"]; 81 + } 82 + for (NSView *sub in target.subviews) { 75 83 if ([sub isKindOfClass:[NSVisualEffectView class]] && 76 - [sub.identifier isEqualToString:@"PocbFloatingVibrancy"]) return; 84 + [sub.identifier isEqualToString:@"PocbFloatingVibrancy"]) { 85 + sub.frame = target.bounds; 86 + return; 87 + } 77 88 } 78 - NSVisualEffectView *vev = makeVev(content.bounds, material); 89 + NSVisualEffectView *vev = makeVev(target.bounds, material); 79 90 vev.identifier = @"PocbFloatingVibrancy"; 80 - [content addSubview:vev positioned:NSWindowBelow relativeTo:nil]; 91 + [target addSubview:vev positioned:NSWindowBelow relativeTo:nil]; 81 92 #else 82 93 (void)window; (void)material; (void)cornerRadius; 94 + #endif 95 + } 96 + 97 + void makeTransparentFloatingPanel(QWidget *window, double cornerRadius) { 98 + #ifdef __APPLE__ 99 + if (!window) return; 100 + window->winId(); 101 + NSWindow *nsw = internal::nsWindowOf(window); 102 + if (!nsw) return; 103 + nsw.opaque = NO; 104 + nsw.backgroundColor = NSColor.clearColor; 105 + nsw.hasShadow = YES; 106 + NSView *content = nsw.contentView; 107 + if (!content) return; 108 + content.wantsLayer = YES; 109 + content.layer.cornerRadius = cornerRadius; 110 + content.layer.masksToBounds = YES; 111 + content.layer.backgroundColor = NSColor.clearColor.CGColor; 112 + if (@available(macOS 10.15, *)) { 113 + [content.layer setValue:@"continuous" forKey:@"cornerCurve"]; 114 + } 115 + #else 116 + (void)window; (void)cornerRadius; 83 117 #endif 84 118 } 85 119
+259
src/services/ChromeExtensionManager.cpp
··· 1 + #include "ChromeExtensionManager.hpp" 2 + 3 + #include "BrowserWindow.hpp" 4 + #include "WebView.hpp" 5 + 6 + #include <QDir> 7 + #include <QFile> 8 + #include <QJsonDocument> 9 + #include <QJsonObject> 10 + #include <QFileInfo> 11 + #include <QRegularExpression> 12 + #include <QSettings> 13 + 14 + #import <Foundation/Foundation.h> 15 + #import <WebKit/WKWebExtension.h> 16 + #import <WebKit/WKWebExtensionContext.h> 17 + #import <WebKit/WKWebExtensionController.h> 18 + #import <WebKit/WKWebExtensionControllerConfiguration.h> 19 + #import <WebKit/WKWebExtensionMatchPattern.h> 20 + #import <WebKit/WKWebExtensionTab.h> 21 + #import <WebKit/WKWebExtensionTabConfiguration.h> 22 + #import <WebKit/WKWebExtensionWindow.h> 23 + #import <WebKit/WKWebExtensionWindowConfiguration.h> 24 + #import <WebKit/WKWebView.h> 25 + 26 + static BrowserWindow *g_browserWindow = nullptr; 27 + 28 + @interface PocbExtensionTab : NSObject <WKWebExtensionTab> 29 + @property(nonatomic, assign) WebView *view; 30 + @end 31 + 32 + @interface PocbExtensionWindow : NSObject <WKWebExtensionWindow> 33 + @end 34 + 35 + @interface PocbExtensionDelegate : NSObject <WKWebExtensionControllerDelegate> 36 + @end 37 + 38 + @implementation PocbExtensionTab 39 + - (WKWebView *)webViewForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return (__bridge WKWebView *)self.view->nativeWebView(); } 40 + - (NSString *)titleForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return self.view->title().toNSString(); } 41 + - (NSUInteger)indexInWindowForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return g_browserWindow ? (NSUInteger)g_browserWindow->extensionViews().indexOf(self.view) : NSNotFound; } 42 + - (NSURL *)pendingURLForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return nil; } 43 + - (BOOL)isLoadingCompleteForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return YES; } 44 + - (void)loadURL:(NSURL *)url forWebExtensionContext:(WKWebExtensionContext *)context completionHandler:(void (^)(NSError *))completionHandler { (void)context; self.view->load(QUrl(QString::fromNSString(url.absoluteString))); completionHandler(nil); } 45 + - (void)reloadFromOrigin:(BOOL)fromOrigin forWebExtensionContext:(WKWebExtensionContext *)context completionHandler:(void (^)(NSError *))completionHandler { (void)context; (void)fromOrigin; self.view->reload(); completionHandler(nil); } 46 + - (void)goBackForWebExtensionContext:(WKWebExtensionContext *)context completionHandler:(void (^)(NSError *))completionHandler { (void)context; self.view->back(); completionHandler(nil); } 47 + - (void)goForwardForWebExtensionContext:(WKWebExtensionContext *)context completionHandler:(void (^)(NSError *))completionHandler { (void)context; self.view->forward(); completionHandler(nil); } 48 + - (void)activateForWebExtensionContext:(WKWebExtensionContext *)context completionHandler:(void (^)(NSError *))completionHandler { (void)context; if (g_browserWindow) g_browserWindow->extensionSelectView(self.view); completionHandler(nil); } 49 + - (BOOL)isSelectedForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return g_browserWindow && g_browserWindow->extensionCurrentView() == self.view; } 50 + - (void)closeForWebExtensionContext:(WKWebExtensionContext *)context completionHandler:(void (^)(NSError *))completionHandler { (void)context; if (g_browserWindow) g_browserWindow->extensionCloseView(self.view); completionHandler(nil); } 51 + @end 52 + 53 + @implementation PocbExtensionWindow 54 + - (NSArray<id<WKWebExtensionTab>> *)tabsForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; NSMutableArray *tabs = [NSMutableArray array]; if (!g_browserWindow) return tabs; for (WebView *view : g_browserWindow->extensionViews()) { PocbExtensionTab *tab = [PocbExtensionTab new]; tab.view = view; [tabs addObject:tab]; } return tabs; } 55 + - (id<WKWebExtensionTab>)activeTabForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; if (!g_browserWindow || !g_browserWindow->extensionCurrentView()) return nil; PocbExtensionTab *tab = [PocbExtensionTab new]; tab.view = g_browserWindow->extensionCurrentView(); return tab; } 56 + - (WKWebExtensionWindowType)windowTypeForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return WKWebExtensionWindowTypeNormal; } 57 + - (WKWebExtensionWindowState)windowStateForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return WKWebExtensionWindowStateNormal; } 58 + - (BOOL)isPrivateForWebExtensionContext:(WKWebExtensionContext *)context { (void)context; return NO; } 59 + - (void)focusForWebExtensionContext:(WKWebExtensionContext *)context completionHandler:(void (^)(NSError *))completionHandler { (void)context; if (g_browserWindow) g_browserWindow->raise(); completionHandler(nil); } 60 + @end 61 + 62 + @implementation PocbExtensionDelegate 63 + - (NSArray<id<WKWebExtensionWindow>> *)webExtensionController:(WKWebExtensionController *)controller openWindowsForExtensionContext:(WKWebExtensionContext *)extensionContext { (void)controller; (void)extensionContext; return g_browserWindow ? @[ [PocbExtensionWindow new] ] : @[]; } 64 + - (id<WKWebExtensionWindow>)webExtensionController:(WKWebExtensionController *)controller focusedWindowForExtensionContext:(WKWebExtensionContext *)extensionContext { (void)controller; (void)extensionContext; return g_browserWindow ? [PocbExtensionWindow new] : nil; } 65 + - (void)webExtensionController:(WKWebExtensionController *)controller openNewTabUsingConfiguration:(WKWebExtensionTabConfiguration *)configuration forExtensionContext:(WKWebExtensionContext *)extensionContext completionHandler:(void (^)(id<WKWebExtensionTab>, NSError *))completionHandler { (void)controller; (void)extensionContext; NSURL *url = [configuration respondsToSelector:@selector(URL)] ? [configuration valueForKey:@"URL"] : nil; WebView *view = g_browserWindow ? g_browserWindow->extensionCreateTab(url ? QUrl(QString::fromNSString(url.absoluteString)) : QUrl(), false) : nullptr; if (!view) { completionHandler(nil, [NSError errorWithDomain:@"pocb.extensions" code:1 userInfo:@{NSLocalizedDescriptionKey:@"No browser window is available"}]); return; } PocbExtensionTab *tab = [PocbExtensionTab new]; tab.view = view; completionHandler(tab, nil); } 66 + - (void)webExtensionController:(WKWebExtensionController *)controller promptForPermissions:(NSSet<WKWebExtensionPermission> *)permissions inTab:(id<WKWebExtensionTab>)tab forExtensionContext:(WKWebExtensionContext *)extensionContext completionHandler:(void (^)(NSSet<WKWebExtensionPermission> *, NSDate *))completionHandler { (void)controller; (void)tab; (void)extensionContext; completionHandler(permissions, [NSDate distantFuture]); } 67 + - (void)webExtensionController:(WKWebExtensionController *)controller promptForPermissionToAccessURLs:(NSSet<NSURL *> *)urls inTab:(id<WKWebExtensionTab>)tab forExtensionContext:(WKWebExtensionContext *)extensionContext completionHandler:(void (^)(NSSet<NSURL *> *, NSDate *))completionHandler { (void)controller; (void)tab; (void)extensionContext; completionHandler(urls, [NSDate distantFuture]); } 68 + - (void)webExtensionController:(WKWebExtensionController *)controller promptForPermissionMatchPatterns:(NSSet<WKWebExtensionMatchPattern *> *)matchPatterns inTab:(id<WKWebExtensionTab>)tab forExtensionContext:(WKWebExtensionContext *)extensionContext completionHandler:(void (^)(NSSet<WKWebExtensionMatchPattern *> *, NSDate *))completionHandler { (void)controller; (void)tab; (void)extensionContext; completionHandler(matchPatterns, [NSDate distantFuture]); } 69 + @end 70 + 71 + ChromeExtensionManager::ChromeExtensionManager(QObject *parent) : QObject(parent) {} 72 + 73 + void ChromeExtensionManager::setBrowserWindow(BrowserWindow *window) { 74 + g_browserWindow = window; 75 + } 76 + 77 + QStringList ChromeExtensionManager::configuredPaths() { 78 + return QSettings().value("extensions/unpackedPaths").toStringList(); 79 + } 80 + 81 + void ChromeExtensionManager::setConfiguredPaths(const QStringList &paths) { 82 + QStringList cleaned; 83 + for (const QString &path : paths) { 84 + const QString p = QDir::cleanPath(path.trimmed()); 85 + if (!p.isEmpty() && QDir(p).exists() && !cleaned.contains(p)) cleaned << p; 86 + } 87 + QSettings().setValue("extensions/unpackedPaths", cleaned); 88 + } 89 + 90 + void *ChromeExtensionManager::nativeController() { 91 + if (@available(macOS 15.4, *)) { 92 + static WKWebExtensionController *controller = nil; 93 + static NSMutableArray<WKWebExtensionContext *> *contexts = nil; 94 + static QStringList loadedPaths; 95 + static quint64 generation = 0; 96 + 97 + const QStringList paths = configuredPaths(); 98 + if (controller && loadedPaths == paths) return (__bridge void *)controller; 99 + 100 + ++generation; 101 + const quint64 thisGeneration = generation; 102 + if (!contexts) contexts = [NSMutableArray array]; 103 + for (WKWebExtensionContext *context in [contexts copy]) { 104 + NSError *unloadError = nil; 105 + if (context.loaded && ![controller unloadExtensionContext:context error:&unloadError]) { 106 + NSLog(@"pocb failed to unload web extension %@: %@", context.webExtension.displayName, unloadError.localizedDescription); 107 + } 108 + } 109 + [contexts removeAllObjects]; 110 + 111 + WKWebExtensionControllerConfiguration *configuration = [WKWebExtensionControllerConfiguration defaultConfiguration]; 112 + controller = [[WKWebExtensionController alloc] initWithConfiguration:configuration]; 113 + static PocbExtensionDelegate *delegate = nil; 114 + if (!delegate) delegate = [PocbExtensionDelegate new]; 115 + controller.delegate = delegate; 116 + loadedPaths = paths; 117 + 118 + for (const QString &path : paths) { 119 + QFileInfo info(path); 120 + if (!info.exists()) { 121 + NSLog(@"pocb skipped missing web extension path %@", path.toNSString()); 122 + continue; 123 + } 124 + if (!info.isDir() && info.suffix().compare(QStringLiteral("zip"), Qt::CaseInsensitive) != 0 && info.suffix().compare(QStringLiteral("crx"), Qt::CaseInsensitive) != 0) { 125 + NSLog(@"pocb skipped unsupported web extension path %@", path.toNSString()); 126 + continue; 127 + } 128 + NSURL *url = [NSURL fileURLWithPath:path.toNSString() isDirectory:info.isDir()]; 129 + [WKWebExtension extensionWithResourceBaseURL:url completionHandler:^(WKWebExtension *extension, NSError *error) { 130 + dispatch_async(dispatch_get_main_queue(), ^{ 131 + if (thisGeneration != generation) return; 132 + if (!extension) { 133 + NSLog(@"pocb failed to load web extension at %@: %@", url.path, error.localizedDescription); 134 + return; 135 + } 136 + for (NSError *parseError in extension.errors) { 137 + NSLog(@"pocb web extension %@ parse issue: %@", extension.displayName ?: url.path, parseError.localizedDescription); 138 + } 139 + WKWebExtensionContext *context = [WKWebExtensionContext contextForExtension:extension]; 140 + NSDate *expirationDate = [NSDate distantFuture]; 141 + NSMutableDictionary *permissions = [NSMutableDictionary dictionary]; 142 + for (WKWebExtensionPermission permission in extension.requestedPermissions) permissions[permission] = expirationDate; 143 + context.grantedPermissions = permissions; 144 + NSMutableDictionary *patterns = [NSMutableDictionary dictionary]; 145 + for (WKWebExtensionMatchPattern *pattern in extension.allRequestedMatchPatterns) patterns[pattern] = expirationDate; 146 + context.grantedPermissionMatchPatterns = patterns; 147 + NSError *loadError = nil; 148 + if (![controller loadExtensionContext:context error:&loadError]) { 149 + NSLog(@"pocb failed to activate web extension %@: %@", extension.displayName ?: url.path, loadError.localizedDescription); 150 + return; 151 + } 152 + [contexts addObject:context]; 153 + }); 154 + }]; 155 + } 156 + return (__bridge void *)controller; 157 + } 158 + return nullptr; 159 + } 160 + 161 + QString ChromeExtensionManager::patternToRegex(const QString &pattern) { 162 + if (pattern == QLatin1String("<all_urls>")) return QStringLiteral("^(https?|file)://"); 163 + QString out; 164 + for (const QChar ch : pattern) { 165 + if (ch == QLatin1Char('*')) { 166 + out += QStringLiteral(".*"); 167 + } else { 168 + out += QRegularExpression::escape(QString(ch)); 169 + } 170 + } 171 + if (out.startsWith(QStringLiteral(".*://"))) out.replace(0, 5, QStringLiteral("https?://")); 172 + return QStringLiteral("^") + out + QStringLiteral("$"); 173 + } 174 + 175 + QList<ChromeExtensionManager::ContentScript> ChromeExtensionManager::loadContentScripts() { 176 + QList<ContentScript> out; 177 + for (const QString &rootPath : configuredPaths()) { 178 + const QDir root(rootPath); 179 + QFile manifestFile(root.filePath("manifest.json")); 180 + if (!manifestFile.open(QIODevice::ReadOnly)) continue; 181 + const QJsonDocument doc = QJsonDocument::fromJson(manifestFile.readAll()); 182 + const QJsonObject manifest = doc.object(); 183 + const QString extensionId = QFileInfo(rootPath).fileName(); 184 + const QJsonArray scripts = manifest.value("content_scripts").toArray(); 185 + for (int i = 0; i < scripts.size(); ++i) { 186 + const QJsonObject scriptObj = scripts.at(i).toObject(); 187 + ContentScript script; 188 + script.id = extensionId + QStringLiteral(":") + QString::number(i); 189 + script.allFrames = scriptObj.value("all_frames").toBool(false); 190 + script.runAt = scriptObj.value("run_at").toString("document_idle"); 191 + for (const QJsonValue &v : scriptObj.value("matches").toArray()) script.matches << patternToRegex(v.toString()); 192 + for (const QJsonValue &v : scriptObj.value("exclude_matches").toArray()) script.excludeMatches << patternToRegex(v.toString()); 193 + for (const QJsonValue &v : scriptObj.value("css").toArray()) { 194 + QFile f(root.filePath(v.toString())); 195 + if (f.open(QIODevice::ReadOnly)) script.css << QString::fromUtf8(f.readAll()); 196 + } 197 + for (const QJsonValue &v : scriptObj.value("js").toArray()) { 198 + QFile f(root.filePath(v.toString())); 199 + if (f.open(QIODevice::ReadOnly)) script.js << QString::fromUtf8(f.readAll()); 200 + } 201 + if (!script.matches.isEmpty() && (!script.js.isEmpty() || !script.css.isEmpty())) out << script; 202 + } 203 + } 204 + return out; 205 + } 206 + 207 + QJsonArray ChromeExtensionManager::contentScriptPayload() { 208 + QJsonArray payload; 209 + for (const ContentScript &script : loadContentScripts()) { 210 + QJsonObject obj; 211 + obj["id"] = script.id; 212 + obj["allFrames"] = script.allFrames; 213 + obj["runAt"] = script.runAt; 214 + QJsonArray matches; 215 + for (const QString &v : script.matches) matches.append(v); 216 + obj["matches"] = matches; 217 + QJsonArray excludeMatches; 218 + for (const QString &v : script.excludeMatches) excludeMatches.append(v); 219 + obj["excludeMatches"] = excludeMatches; 220 + QJsonArray css; 221 + for (const QString &v : script.css) css.append(v); 222 + obj["css"] = css; 223 + QJsonArray js; 224 + for (const QString &v : script.js) js.append(v); 225 + obj["js"] = js; 226 + payload.append(obj); 227 + } 228 + return payload; 229 + } 230 + 231 + QString ChromeExtensionManager::bootstrapScript() { 232 + const QString payload = QString::fromUtf8(QJsonDocument(contentScriptPayload()).toJson(QJsonDocument::Compact)); 233 + return QStringLiteral(R"JS( 234 + (function(){ 235 + if (window.__pocbChromeExtensionsInstalled) return; 236 + window.__pocbChromeExtensionsInstalled = true; 237 + var scripts = __POCB_PAYLOAD__; 238 + window.chrome = window.chrome || {}; 239 + chrome.runtime = chrome.runtime || { id: 'pocb', getURL: function(path){ return path || ''; }, sendMessage: function(){ var cb = arguments[arguments.length - 1]; if (typeof cb === 'function') cb(undefined); } }; 240 + chrome.storage = chrome.storage || {}; 241 + chrome.storage.local = chrome.storage.local || { 242 + get: function(keys, cb){ var data = {}; try { data = JSON.parse(localStorage.getItem('__pocb_extension_storage') || '{}'); } catch(e) {} if (typeof cb === 'function') cb(data); }, 243 + set: function(items, cb){ var data = {}; try { data = JSON.parse(localStorage.getItem('__pocb_extension_storage') || '{}'); } catch(e) {} Object.assign(data, items || {}); localStorage.setItem('__pocb_extension_storage', JSON.stringify(data)); if (typeof cb === 'function') cb(); }, 244 + remove: function(keys, cb){ var data = {}; try { data = JSON.parse(localStorage.getItem('__pocb_extension_storage') || '{}'); } catch(e) {} (Array.isArray(keys) ? keys : [keys]).forEach(function(k){ delete data[k]; }); localStorage.setItem('__pocb_extension_storage', JSON.stringify(data)); if (typeof cb === 'function') cb(); } 245 + }; 246 + function ok(script){ 247 + var href = location.href; 248 + var match = script.matches.some(function(p){ try { return new RegExp(p).test(href); } catch(e) { return false; } }); 249 + var excluded = script.excludeMatches.some(function(p){ try { return new RegExp(p).test(href); } catch(e) { return false; } }); 250 + return match && !excluded; 251 + } 252 + scripts.forEach(function(script){ 253 + if (!ok(script)) return; 254 + script.css.forEach(function(css){ var style = document.createElement('style'); style.textContent = css; (document.head || document.documentElement).appendChild(style); }); 255 + script.js.forEach(function(code){ try { (0, eval)(code); } catch(e) { console.error('pocb extension script failed', script.id, e); } }); 256 + }); 257 + })(); 258 + )JS").replace(QStringLiteral("__POCB_PAYLOAD__"), payload); 259 + }
+37
src/services/ChromeExtensionManager.hpp
··· 1 + #pragma once 2 + 3 + #include <QJsonArray> 4 + #include <QObject> 5 + #include <QString> 6 + #include <QStringList> 7 + 8 + class BrowserWindow; 9 + 10 + class ChromeExtensionManager final : public QObject { 11 + Q_OBJECT 12 + public: 13 + struct ContentScript { 14 + QString id; 15 + QStringList matches; 16 + QStringList excludeMatches; 17 + QStringList js; 18 + QStringList css; 19 + bool allFrames = false; 20 + QString runAt; 21 + }; 22 + 23 + explicit ChromeExtensionManager(QObject *parent = nullptr); 24 + 25 + static QStringList configuredPaths(); 26 + static void setConfiguredPaths(const QStringList &paths); 27 + static QJsonArray contentScriptPayload(); 28 + static QString bootstrapScript(); 29 + static void setBrowserWindow(BrowserWindow *window); 30 + // Returns a `WKWebExtensionController *` on macOS 15.4+ after starting 31 + // asynchronous loading for configured unpacked extensions. 32 + static void *nativeController(); 33 + 34 + private: 35 + static QList<ContentScript> loadContentScripts(); 36 + static QString patternToRegex(const QString &pattern); 37 + };
+94 -11
src/tabs/TabTree.cpp
··· 10 10 #include <QHeaderView> 11 11 #include <QIcon> 12 12 #include <QMouseEvent> 13 + #include <QFontMetrics> 13 14 #include <QPainter> 14 15 #include <QPixmap> 15 16 #include <QStackedLayout> ··· 30 31 side); 31 32 } 32 33 34 + int itemDepth(const QTreeWidget *tree, const QModelIndex &index) { 35 + if (!tree) return 0; 36 + int depth = 0; 37 + QTreeWidgetItem *item = tree->itemFromIndex(index); 38 + while (item && item->parent()) { 39 + ++depth; 40 + item = item->parent(); 41 + } 42 + return depth; 43 + } 44 + 33 45 class TabItemDelegate final : public QStyledItemDelegate { 34 46 public: 35 47 TabItemDelegate(const Theme &theme, QObject *parent) 36 - : QStyledItemDelegate(parent), m_closeIcon(mac::sfSymbolIcon("xmark", 10.5, theme.foreground)) {} 48 + : QStyledItemDelegate(parent), m_theme(theme), m_closeIcon(mac::sfSymbolIcon("xmark", 10.5, theme.foreground)) {} 37 49 38 50 void paint(QPainter *painter, const QStyleOptionViewItem &option, 39 51 const QModelIndex &index) const override { 40 - QStyledItemDelegate::paint(painter, option, index); 52 + const auto *tree = option.widget ? qobject_cast<const QTreeWidget *>(option.widget) : nullptr; 53 + const bool selected = tree && tree->currentIndex() == index; 54 + const bool hovered = option.state.testFlag(QStyle::State_MouseOver); 55 + const int depth = itemDepth(tree, index); 41 56 const int viewportWidth = option.widget ? option.widget->width() : 0; 42 57 const QRect closeRect = closeButtonRect(option.rect, viewportWidth); 58 + 59 + painter->save(); 60 + painter->setRenderHint(QPainter::Antialiasing, true); 61 + if (selected || hovered) { 62 + QColor fill = m_theme.background.lightness() < 128 ? QColor(255, 255, 255) : QColor(0, 0, 0); 63 + fill.setAlpha(selected ? 22 : 12); 64 + QRect rowRect = option.rect; 65 + rowRect.setLeft(6 + depth * 18); 66 + if (viewportWidth > 0) rowRect.setRight(viewportWidth - 4); 67 + painter->setPen(Qt::NoPen); 68 + painter->setBrush(fill); 69 + painter->drawRoundedRect(rowRect.adjusted(0, 2, 0, -2), 7, 7); 70 + } 71 + 72 + const QVariant decoration = index.data(Qt::DecorationRole); 73 + QRect textRect = option.rect.adjusted(14 + depth * 18, 0, -30, 0); 74 + if (decoration.canConvert<QIcon>()) { 75 + const QIcon icon = qvariant_cast<QIcon>(decoration); 76 + const QRect iconRect(textRect.left(), option.rect.top() + (option.rect.height() - 16) / 2, 16, 16); 77 + icon.paint(painter, iconRect, Qt::AlignCenter); 78 + textRect.setLeft(iconRect.right() + 7); 79 + } 80 + 81 + painter->setPen(m_theme.foreground); 82 + painter->setFont(option.font); 83 + const QString text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, textRect.width()); 84 + painter->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, text); 85 + painter->restore(); 86 + 87 + if (!selected && !hovered) return; 43 88 if (!m_closeIcon.isNull()) { 44 89 m_closeIcon.paint(painter, closeRect.adjusted(4, 4, -4, -4), Qt::AlignCenter); 45 90 } else { ··· 54 99 } 55 100 56 101 private: 102 + Theme m_theme; 57 103 QIcon m_closeIcon; 58 104 }; 59 105 106 + class TabTreeWidget final : public QTreeWidget { 107 + public: 108 + using QTreeWidget::QTreeWidget; 109 + 110 + protected: 111 + void drawBranches(QPainter *, const QRect &, const QModelIndex &) const override {} 112 + }; 113 + 60 114 } // namespace 61 115 62 116 TabTree::TabTree(ProfileStore &profiles, FaviconService *favicons, QWidget *stack, 63 117 const Theme &theme, QWidget *sidebarParent, QObject *parent) 64 118 : QObject(parent), m_profiles(&profiles), m_favicons(favicons), 65 119 m_stack(stack), m_theme(theme) { 66 - m_tabs = new QTreeWidget(sidebarParent); 120 + m_tabs = new TabTreeWidget(sidebarParent); 67 121 m_tabs->setObjectName("TabTree"); 68 122 m_tabs->setHeaderHidden(true); 69 123 m_tabs->header()->setStretchLastSection(true); 70 124 m_tabs->setIndentation(14); 71 125 m_tabs->setRootIsDecorated(false); 126 + m_tabs->setAllColumnsShowFocus(false); 127 + m_tabs->setSelectionMode(QAbstractItemView::NoSelection); 128 + m_tabs->setFocusPolicy(Qt::NoFocus); 72 129 m_tabs->setAnimated(true); 73 130 m_tabs->setFrameShape(QFrame::NoFrame); 74 131 m_tabs->setIconSize(QSize(16, 16)); ··· 81 138 m_tabs->setAttribute(Qt::WA_MacShowFocusRect, false); 82 139 m_tabs->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); 83 140 m_tabs->setAttribute(Qt::WA_TranslucentBackground); 141 + m_tabs->setAttribute(Qt::WA_NoSystemBackground); 142 + m_tabs->viewport()->setAttribute(Qt::WA_TranslucentBackground); 143 + m_tabs->viewport()->setAttribute(Qt::WA_NoSystemBackground); 84 144 m_tabs->viewport()->setAutoFillBackground(false); 85 145 m_tabs->setStyleSheet(QString( 86 - "QTreeWidget#TabTree { background: transparent; border: none; color: %1; }" 87 - "QTreeWidget#TabTree::item { padding: 4px 28px 4px 6px; border-radius: 6px; color: %1; }" 88 - "QTreeWidget#TabTree::item:selected { background: %2; color: %1; }" 89 - "QTreeWidget#TabTree::item:hover:!selected { background: %3; }") 90 - .arg(m_theme.foreground.name(), 91 - m_theme.raised.name(), 92 - m_theme.hover.name())); 146 + "QTreeWidget#TabTree { background: transparent; border: none; color: %1; outline: 0; }" 147 + "QTreeWidget#TabTree::item { padding: 4px 28px 4px 6px; border: none; background: transparent; color: %1; selection-background-color: transparent; }" 148 + "QTreeWidget#TabTree::item:selected { background: transparent; color: %1; selection-background-color: transparent; }" 149 + "QTreeWidget#TabTree::item:selected:active { background: transparent; color: %1; selection-background-color: transparent; }" 150 + "QTreeWidget#TabTree::item:selected:!active { background: transparent; color: %1; selection-background-color: transparent; }" 151 + "QTreeWidget#TabTree::item:hover:!selected { background: transparent; }" 152 + "QTreeWidget#TabTree::branch { background: transparent; border: none; image: none; }" 153 + "QTreeWidget#TabTree::branch:selected { background: transparent; border: none; image: none; }" 154 + "QTreeWidget#TabTree::branch:hover { background: transparent; border: none; image: none; }") 155 + .arg(m_theme.foreground.name())); 93 156 94 157 connect(m_tabs, &QTreeWidget::currentItemChanged, this, [this] { 95 158 emit currentTabChanged(); ··· 127 190 return m_tabs->currentItem(); 128 191 } 129 192 193 + QList<WebView *> TabTree::views() const { 194 + return m_views.values(); 195 + } 196 + 197 + void TabTree::selectView(WebView *view) { 198 + for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { 199 + if (it.value() == view) { 200 + selectItem(it.key()); 201 + return; 202 + } 203 + } 204 + } 205 + 130 206 void TabTree::selectItem(QTreeWidgetItem *item) { 131 207 if (!item) return; 132 208 m_tabs->setCurrentItem(item); 209 + m_tabs->clearSelection(); 210 + m_tabs->viewport()->update(); 133 211 emit currentTabChanged(); 134 212 } 135 213 ··· 163 241 return QObject::eventFilter(watched, event); 164 242 } 165 243 166 - void TabTree::newTab(const QUrl &url, bool background, QTreeWidgetItem *parentItem) { 244 + WebView *TabTree::newTabForExtension(const QUrl &url, bool background, QTreeWidgetItem *parentItem) { 167 245 auto *view = new WebView(m_profiles->currentProfile(), m_stack); 168 246 169 247 auto *item = new QTreeWidgetItem(QStringList() << "New tab"); ··· 179 257 180 258 view->load(url.isEmpty() ? QUrl(m_homePage) : url); 181 259 if (!background) selectItem(item); 260 + return view; 261 + } 262 + 263 + void TabTree::newTab(const QUrl &url, bool background, QTreeWidgetItem *parentItem) { 264 + newTabForExtension(url, background, parentItem); 182 265 } 183 266 184 267 void TabTree::adoptChildView(WebView *child, QTreeWidgetItem *parentItem, bool background) {
+5
src/tabs/TabTree.hpp
··· 4 4 5 5 #include <QColor> 6 6 #include <QHash> 7 + #include <QList> 7 8 #include <QObject> 8 9 #include <QString> 9 10 #include <QUrl> ··· 24 25 QTreeWidget *widget() const { return m_tabs; } 25 26 WebView *currentView() const; 26 27 QTreeWidgetItem *currentItem() const; 28 + QList<WebView *> views() const; 29 + void selectView(WebView *view); 27 30 31 + WebView *newTabForExtension(const QUrl &url = QUrl(), bool background = false, 32 + QTreeWidgetItem *parentItem = nullptr); 28 33 void newTab(const QUrl &url = QUrl(), bool background = false, 29 34 QTreeWidgetItem *parentItem = nullptr); 30 35 void closeCurrent();
+58 -22
src/ui/AddressBarController.cpp
··· 6 6 #include <QPainter> 7 7 #include <QPainterPath> 8 8 #include <QPixmap> 9 + #include <QRegion> 9 10 #include <QVBoxLayout> 10 11 #include <QSettings> 11 12 #include <QEvent> ··· 59 60 p.drawPath(path); 60 61 } 61 62 63 + void resizeEvent(QResizeEvent *e) override { 64 + QWidget::resizeEvent(e); 65 + QPainterPath path; 66 + path.addRoundedRect(QRectF(rect()), 12.0, 12.0); 67 + setMask(QRegion(path.toFillPolygon().toPolygon())); 68 + } 69 + 62 70 private: 63 71 QColor m_fill; 64 72 QColor m_border; ··· 70 78 QList<QPair<QString, QString>> extraParams; 71 79 QString queryParam = "q"; 72 80 }; 81 + QString colorWithAlpha(QColor color, float alpha) { 82 + color.setAlphaF(alpha); 83 + return QString("rgba(%1, %2, %3, %4)") 84 + .arg(color.red()) 85 + .arg(color.green()) 86 + .arg(color.blue()) 87 + .arg(QString::number(color.alphaF(), 'f', 3)); 88 + } 89 + 73 90 AddrEngine engineForHost(const QString &host) { 74 91 const QString h = host.toLower(); 75 92 if (h.contains("duckduckgo")) return {"duckduckgo.com", "/ac/", {{"type","list"}}, "q"}; ··· 431 448 // widget itself sits inside as a transparent child, so its rows 432 449 // can be painted/styled independently of the panel chrome. 433 450 QColor fill = m_theme.panel; 434 - fill.setAlphaF(0.08); 451 + fill.setAlphaF(0.38); 435 452 QColor border = m_theme.border; 436 - border.setAlpha(120); 453 + border.setAlpha(105); 437 454 m_popup = new AddrPopupFrame(fill, border); 438 455 m_popup->setParent(m_bar ? m_bar->window() : nullptr); 439 - m_popup->setWindowFlags(Qt::Widget); 456 + m_popup->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::WindowDoesNotAcceptFocus); 440 457 m_popup->setAttribute(Qt::WA_TranslucentBackground); 441 458 m_popup->setAttribute(Qt::WA_NoSystemBackground); 442 459 m_popup->setAutoFillBackground(false); 443 460 m_popup->setFocusPolicy(Qt::NoFocus); 444 461 445 462 auto *vbox = new QVBoxLayout(m_popup); 446 - vbox->setContentsMargins(6, 6, 6, 6); 463 + vbox->setContentsMargins(0, 0, 0, 0); 447 464 vbox->setSpacing(0); 448 465 449 466 m_popupList = new QListWidget(m_popup); 467 + m_popupList->setAttribute(Qt::WA_NativeWindow); 450 468 vbox->addWidget(m_popupList); 451 469 452 470 m_popupList->setObjectName("AddrPopup"); ··· 455 473 m_popupList->setUniformItemSizes(false); 456 474 m_popupList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 457 475 m_popupList->setAttribute(Qt::WA_TranslucentBackground); 476 + m_popupList->setAttribute(Qt::WA_NoSystemBackground); 477 + m_popupList->setAutoFillBackground(false); 478 + m_popupList->viewport()->setAttribute(Qt::WA_NativeWindow); 479 + m_popupList->viewport()->setAttribute(Qt::WA_TranslucentBackground); 480 + m_popupList->viewport()->setAttribute(Qt::WA_NoSystemBackground); 458 481 m_popupList->viewport()->setAutoFillBackground(false); 459 482 QFont f = m_popupList->font(); 460 483 f.setFamily(m_theme.fontFamily); 461 484 f.setPointSize(m_theme.regularSize); 462 485 m_popupList->setFont(f); 463 486 m_popupList->setStyleSheet(QString( 464 - "QListWidget#AddrPopup { background: transparent; border: none; padding: 0px; color: %1; }" 487 + "QListWidget#AddrPopup, QListWidget#AddrPopup::viewport { background: transparent; border: none; padding: 0px; color: %1; }" 465 488 "QListWidget#AddrPopup::item {" 466 - " padding: 6px 10px;" 467 - " margin: 1px 2px;" 489 + " padding: 8px 14px;" 490 + " margin: 0px;" 468 491 " border-radius: 6px;" 469 492 " color: %1;" 470 493 "}" ··· 474 497 "QListWidget#AddrPopup QScrollBar::handle:vertical { background: %4; border-radius: 3px; min-height: 24px; }" 475 498 "QListWidget#AddrPopup QScrollBar::add-line, QListWidget#AddrPopup QScrollBar::sub-line { height:0; width:0; }") 476 499 .arg(m_theme.foreground.name(), 477 - m_theme.raised.name(), 478 - m_theme.hover.name(), 479 - m_theme.border.name())); 500 + colorWithAlpha(m_theme.raised, 0.70f), 501 + colorWithAlpha(m_theme.hover, 0.54f), 502 + colorWithAlpha(m_theme.border, 0.62f))); 480 503 connect(m_popupList, &QListWidget::itemClicked, this, [this](QListWidgetItem *it) { 481 504 if (!it) return; 482 505 // Skip non-selectable header row. ··· 498 521 auto *it = new QListWidgetItem(s, m_popupList); 499 522 it->setData(Qt::UserRole, s); 500 523 it->setIcon(engineIcon()); 501 - it->setSizeHint(QSize(0, 30)); 524 + it->setSizeHint(QSize(0, 34)); 502 525 } 503 526 m_popupList->setCurrentRow(-1); 504 527 showPopup(); ··· 511 534 && anchor->window()->findChild<QWidget *>("WebTopbar")->isAncestorOf(anchor); 512 535 513 536 const QPoint anchorBottom = anchor->mapToGlobal(QPoint(0, anchor->height())); 514 - const QPoint parentBottom = m_popup->parentWidget() 515 - ? m_popup->parentWidget()->mapFromGlobal(anchorBottom) 516 - : anchorBottom; 517 - const QRect available = m_popup->parentWidget() 518 - ? m_popup->parentWidget()->rect() 519 - : QRect(); 537 + const QPoint parentBottom = m_popup->isWindow() 538 + ? anchorBottom 539 + : (m_popup->parentWidget() 540 + ? m_popup->parentWidget()->mapFromGlobal(anchorBottom) 541 + : anchorBottom); 542 + const QRect available = m_popup->isWindow() 543 + ? (anchor->screen() ? anchor->screen()->availableGeometry() : QRect()) 544 + : (m_popup->parentWidget() 545 + ? m_popup->parentWidget()->rect() 546 + : QRect()); 520 547 521 548 const int rows = qMin(m_popupList ? m_popupList->count() : 0, 9); 522 - const int height = qMax(1, rows) * 30 + 12; 549 + const int height = qMax(1, rows) * 34; 523 550 int width = inTopbar ? qBound(420, anchor->width(), 720) 524 551 : qMax(anchor->width(), 320); 525 552 int x = parentBottom.x() + (anchor->width() - width) / 2; ··· 538 565 void AddressBarController::showPopup() { 539 566 if (!m_popup) return; 540 567 positionPopup(); 541 - if (!m_popup->isVisible()) { 542 - m_popup->show(); 543 - mac::applyVibrancyBehind(m_popup, mac::VibrancyMaterial::Popover); 568 + if (m_bar && !m_bar->hasFocus()) m_bar->setFocus(Qt::OtherFocusReason); 569 + if (!m_popup->isVisible()) m_popup->show(); 570 + mac::makeFloatingVibrantPanel(m_popup, mac::VibrancyMaterial::Popover, 12.0); 571 + mac::roundWidgetCorners(m_popup, 12.0, false); 572 + if (m_popupList) { 573 + m_popupList->winId(); 574 + m_popupList->viewport()->winId(); 575 + m_popupList->raise(); 576 + m_popupList->viewport()->raise(); 544 577 } 545 578 m_popup->raise(); 546 - if (m_bar && !m_bar->hasFocus()) m_bar->setFocus(Qt::OtherFocusReason); 579 + if (m_bar) { 580 + m_bar->setFocus(Qt::OtherFocusReason); 581 + m_bar->update(); 582 + } 547 583 } 548 584 549 585 void AddressBarController::hidePopup() {
+23 -10
src/ui/FloatingOmnibox.cpp
··· 31 31 constexpr int kInputPadX = 16; // SearchBar.qml leftMargin/rightMargin 32 32 constexpr int kListPadV = 4; // GenericListView topMargin/bottomMargin 33 33 constexpr int kItemMarginX = 6; // SelectableDelegate left/rightMargin 34 - constexpr float kFillAlpha = 0.62f; // Config.windowOpacity-ish, tuned for vibrancy 34 + constexpr float kFillAlpha = 0.42f; // Config.windowOpacity-ish, tuned for vibrancy 35 + constexpr float kDividerAlpha = 0.48f; 36 + constexpr float kHoverAlpha = 0.46f; 37 + constexpr float kSelectedAlpha = 0.64f; 38 + constexpr float kScrollbarAlpha = 0.58f; 39 + 40 + QString colorWithAlpha(QColor color, float alpha) { 41 + color.setAlphaF(alpha); 42 + return QString("rgba(%1, %2, %3, %4)") 43 + .arg(color.red()) 44 + .arg(color.green()) 45 + .arg(color.blue()) 46 + .arg(QString::number(color.alphaF(), 'f', 3)); 47 + } 35 48 36 49 struct EngineSuggest { 37 50 QString host; ··· 104 117 // 1px hairline divider (only when suggestions are present). 105 118 m_divider = new QWidget(this); 106 119 m_divider->setFixedHeight(1); 107 - m_divider->setStyleSheet(QString("background: %1;").arg(theme.borderSoft.name())); 120 + m_divider->setStyleSheet(QString("background: %1;").arg(colorWithAlpha(theme.borderSoft, kDividerAlpha))); 108 121 m_divider->hide(); 109 122 col->addWidget(m_divider); 110 123 ··· 158 171 .arg(QString::number(kListPadV), 159 172 theme.foreground.name(), 160 173 QString::number(kItemMarginX), 161 - theme.raised.name(), 162 - theme.hover.name(), 163 - theme.border.name())); 174 + colorWithAlpha(theme.raised, kSelectedAlpha), 175 + colorWithAlpha(theme.hover, kHoverAlpha), 176 + colorWithAlpha(theme.border, kScrollbarAlpha))); 164 177 m_list->hide(); 165 178 col->addWidget(m_list); 166 179 ··· 210 223 211 224 void FloatingOmnibox::showEvent(QShowEvent *e) { 212 225 QWidget::showEvent(e); 213 - // Real translucency: NSVisualEffectView (Popover material) behind our 214 - // painted fill, with the NSView layer corner-radius'd so the blur 215 - // clips to the rounded shape. No-op off macOS. 216 - mac::applyVibrancyBehind(this, mac::VibrancyMaterial::Popover); 217 - mac::roundWidgetCorners(this, kPanelRadius); 226 + // Real translucency: make the popup's native NSWindow non-opaque and 227 + // clip the AppKit vibrancy view to the same rounded content shape. 228 + // No-op off macOS. 229 + mac::makeFloatingVibrantPanel(this, mac::VibrancyMaterial::Popover, kPanelRadius); 230 + mac::roundWidgetCorners(this, kPanelRadius, false); 218 231 m_input->setFocus(Qt::ShortcutFocusReason); 219 232 } 220 233
+1
src/web/WebView.hpp
··· 21 21 void reload(); 22 22 QUrl url() const; 23 23 QString title() const; 24 + void *nativeWebView() const; 24 25 25 26 // Internal: install an externally-created WKWebView (used by the 26 27 // WKUIDelegate's createWebViewWithConfiguration: path so popups stay
+23
src/web/WebView.mm
··· 1 1 #include "WebView.hpp" 2 2 3 3 #include "WebKitProfile.hpp" 4 + #include "ChromeExtensionManager.hpp" 4 5 5 6 #import <AppKit/AppKit.h> 6 7 #import <WebKit/WKWebView.h> ··· 13 14 #import <WebKit/WKNavigationAction.h> 14 15 #import <WebKit/WKWindowFeatures.h> 15 16 #import <WebKit/WKPreferences.h> 17 + #import <WebKit/WKWebExtensionController.h> 18 + #import <WebKit/WKUserContentController.h> 19 + #import <WebKit/WKUserScript.h> 16 20 17 21 static void disableWebKit60FpsCap(WKPreferences *preferences) { 18 22 if (!preferences) return; ··· 210 214 if (profile->dataStore()) { 211 215 cfg.websiteDataStore = (__bridge WKWebsiteDataStore *)profile->dataStore(); 212 216 } 217 + if (@available(macOS 15.4, *)) { 218 + if (void *controller = ChromeExtensionManager::nativeController()) { 219 + cfg.webExtensionController = (__bridge WKWebExtensionController *)controller; 220 + } 221 + } else { 222 + const QString extensionBootstrap = ChromeExtensionManager::bootstrapScript(); 223 + if (!extensionBootstrap.trimmed().isEmpty()) { 224 + WKUserContentController *content = [[WKUserContentController alloc] init]; 225 + WKUserScript *script = [[WKUserScript alloc] initWithSource:extensionBootstrap.toNSString() 226 + injectionTime:WKUserScriptInjectionTimeAtDocumentStart 227 + forMainFrameOnly:NO]; 228 + [content addUserScript:script]; 229 + cfg.userContentController = content; 230 + } 231 + } 213 232 WKWebView *wk = [[WKWebView alloc] initWithFrame:NSZeroRect configuration:cfg]; 214 233 if (@available(macOS 13.3, *)) { 215 234 wk.inspectable = YES; ··· 304 323 QString WebView::title() const { 305 324 if (!m_impl->wk || !m_impl->wk.title) return QString(); 306 325 return QString::fromNSString(m_impl->wk.title); 326 + } 327 + 328 + void *WebView::nativeWebView() const { 329 + return (__bridge void *)m_impl->wk; 307 330 } 308 331 309 332 QColor WebView::cachedThemeColor() const {