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 5db88081 85aa85c2

+237 -77
+7
CMakeLists.txt
··· 88 88 ) 89 89 90 90 if(APPLE AND TARGET Qt6::QCocoaIntegrationPlugin) 91 + get_target_property(_qt_qmake Qt6::qmake IMPORTED_LOCATION) 92 + get_filename_component(_qt_bin_dir "${_qt_qmake}" DIRECTORY) 93 + get_filename_component(_qt_prefix "${_qt_bin_dir}" DIRECTORY) 94 + set(_qt_openssl_backend "${_qt_prefix}/share/qt/plugins/tls/libqopensslbackend.dylib") 95 + 91 96 add_custom_command(TARGET pocb POST_BUILD 92 97 COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_BUNDLE_DIR:pocb>/Contents/PlugIns/platforms" 93 98 COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:Qt6::QCocoaIntegrationPlugin>" "$<TARGET_BUNDLE_DIR:pocb>/Contents/PlugIns/platforms/libqcocoa.dylib" 99 + COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_BUNDLE_DIR:pocb>/Contents/PlugIns/tls" 100 + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_qt_openssl_backend}" "$<TARGET_BUNDLE_DIR:pocb>/Contents/PlugIns/tls/libqopensslbackend.dylib" 94 101 COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_BUNDLE_DIR:pocb>/Contents/Resources" 95 102 COMMAND ${CMAKE_COMMAND} -E echo "[Paths]" > "$<TARGET_BUNDLE_DIR:pocb>/Contents/Resources/qt.conf" 96 103 COMMAND ${CMAKE_COMMAND} -E echo "Plugins = PlugIns" >> "$<TARGET_BUNDLE_DIR:pocb>/Contents/Resources/qt.conf"
+3
src/app/BrowserWindow.cpp
··· 558 558 }); 559 559 connect(m_tabTree, &TabTree::themeColorChanged, this, 560 560 &BrowserWindow::applyChromeForPageColor); 561 + connect(m_tabTree, &TabTree::contentMouseDown, this, [this] { 562 + if (m_addressBarCtl && m_addressBarCtl->isEditing()) m_addressBarCtl->cancelEditing(); 563 + }); 561 564 connect(&m_profiles, &ProfileStore::currentProfileChanged, this, [this] { 562 565 m_tabTree->rebuildForProfile(); 563 566 });
+88 -6
src/services/FaviconService.cpp
··· 1 1 #include "FaviconService.hpp" 2 2 3 + #include <QColor> 3 4 #include <QFile> 4 5 #include <QFileInfo> 6 + #include <QImage> 5 7 #include <QNetworkAccessManager> 6 8 #include <QNetworkReply> 7 9 #include <QNetworkRequest> 8 10 9 11 namespace { 12 + constexpr int kDirectAttemptCount = 4; 10 13 constexpr int kSizes[] = {64, 32, 16}; 11 14 constexpr int kSizeCount = sizeof(kSizes) / sizeof(kSizes[0]); 15 + 16 + bool nearWhite(const QColor &c) { 17 + return c.alpha() > 220 && c.red() > 232 && c.green() > 232 && c.blue() > 232; 18 + } 19 + 20 + QString registrableDomain(QString domain) { 21 + if (domain.startsWith("www.")) domain.remove(0, 4); 22 + const QStringList parts = domain.split('.', Qt::SkipEmptyParts); 23 + if (parts.size() < 2) return domain; 24 + return parts.mid(parts.size() - 2).join('.'); 25 + } 26 + 27 + QPixmap removeWhiteMatte(const QPixmap &pm) { 28 + QImage img = pm.toImage().convertToFormat(QImage::Format_ARGB32); 29 + if (img.isNull()) return pm; 30 + 31 + QVector<QPoint> stack; 32 + auto push = [&](int x, int y) { 33 + if (x < 0 || y < 0 || x >= img.width() || y >= img.height()) return; 34 + QColor c = img.pixelColor(x, y); 35 + if (!nearWhite(c)) return; 36 + c.setAlpha(0); 37 + img.setPixelColor(x, y, c); 38 + stack.append(QPoint(x, y)); 39 + }; 40 + 41 + for (int x = 0; x < img.width(); ++x) { 42 + push(x, 0); 43 + push(x, img.height() - 1); 44 + } 45 + for (int y = 0; y < img.height(); ++y) { 46 + push(0, y); 47 + push(img.width() - 1, y); 48 + } 49 + 50 + while (!stack.isEmpty()) { 51 + const QPoint p = stack.takeLast(); 52 + push(p.x() + 1, p.y()); 53 + push(p.x() - 1, p.y()); 54 + push(p.x(), p.y() + 1); 55 + push(p.x(), p.y() - 1); 56 + } 57 + 58 + for (int y = 0; y < img.height(); ++y) { 59 + for (int x = 0; x < img.width(); ++x) { 60 + QColor c = img.pixelColor(x, y); 61 + if (!nearWhite(c)) continue; 62 + int transparentNeighbors = 0; 63 + for (int dy = -1; dy <= 1; ++dy) { 64 + for (int dx = -1; dx <= 1; ++dx) { 65 + if (dx == 0 && dy == 0) continue; 66 + const int nx = x + dx; 67 + const int ny = y + dy; 68 + if (nx < 0 || ny < 0 || nx >= img.width() || ny >= img.height()) continue; 69 + if (img.pixelColor(nx, ny).alpha() < 16) ++transparentNeighbors; 70 + } 71 + } 72 + if (transparentNeighbors > 0) { 73 + c.setAlpha(0); 74 + img.setPixelColor(x, y, c); 75 + } 76 + } 77 + } 78 + 79 + return QPixmap::fromImage(img); 80 + } 12 81 } 13 82 14 83 FaviconService::FaviconService(const QDir &cacheDir, QObject *parent) ··· 40 109 } 41 110 42 111 void FaviconService::store(const QString &domain, const QPixmap &pm) { 43 - m_memCache.insert(domain, pm); 44 - pm.save(diskPath(domain), "PNG"); 45 - emit faviconReady(domain, pm); 112 + const QPixmap cleaned = removeWhiteMatte(pm); 113 + m_memCache.insert(domain, cleaned); 114 + cleaned.save(diskPath(domain), "PNG"); 115 + emit faviconReady(domain, cleaned); 46 116 } 47 117 48 118 void FaviconService::request(const QUrl &url) { ··· 62 132 63 133 void FaviconService::tryNextSize(const QString &domain) { 64 134 const int idx = m_attempt.value(domain, 0); 65 - if (idx >= kSizeCount) { 135 + if (idx >= kDirectAttemptCount + kSizeCount) { 66 136 m_attempt.remove(domain); 67 137 m_inflight.remove(domain); 68 138 return; 69 139 } 70 140 71 - const int size = kSizes[idx]; 72 - const QUrl url(QString("https://www.google.com/s2/favicons?domain=%1&sz=%2").arg(domain).arg(size)); 141 + QUrl url; 142 + const QString rootDomain = registrableDomain(domain); 143 + if (idx == 0) { 144 + url = QUrl(QString("https://%1/favicon.ico").arg(domain)); 145 + } else if (idx == 1) { 146 + url = QUrl(QString("https://www.%1/favicon.ico").arg(domain)); 147 + } else if (idx == 2) { 148 + url = QUrl(QString("https://%1/favicon.ico").arg(rootDomain)); 149 + } else if (idx == 3) { 150 + url = QUrl(QString("https://www.%1/favicon.ico").arg(rootDomain)); 151 + } else { 152 + const int size = kSizes[idx - kDirectAttemptCount]; 153 + url = QUrl(QString("https://www.google.com/s2/favicons?domain=%1&sz=%2").arg(rootDomain).arg(size)); 154 + } 73 155 QNetworkRequest req(url); 74 156 req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); 75 157 auto *reply = m_nam->get(req);
+5 -1
src/tabs/TabTree.cpp
··· 111 111 if (pm.isNull()) return; 112 112 const QIcon icon(pm); 113 113 for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { 114 - const QString itemDomain = it.value()->url().host(); 114 + QString itemDomain = it.value()->url().host(); 115 + if (itemDomain.startsWith("www.")) itemDomain.remove(0, 4); 115 116 if (itemDomain == domain) it.key()->setIcon(0, icon); 116 117 } 117 118 }); ··· 240 241 emit themeColorChanged(c); 241 242 }); 242 243 connect(view, &WebView::loadFinished, this, [this](bool) { emit currentTabChanged(); }); 244 + connect(view, &WebView::contentMouseDown, this, [this, view] { 245 + if (view == currentView()) emit contentMouseDown(); 246 + }); 243 247 connect(view, &WebView::newTabRequested, this, [this, item](WebView *child, bool background) { 244 248 adoptChildView(child, item, background); 245 249 });
+1
src/tabs/TabTree.hpp
··· 44 44 // navigation finishes with the page's preferred chrome colour. Invalid 45 45 // QColor when the page exposes nothing useful. 46 46 void themeColorChanged(const QColor &color); 47 + void contentMouseDown(); 47 48 48 49 private: 49 50 bool eventFilter(QObject *watched, QEvent *event) override;
+74 -69
src/ui/AddressBarController.cpp
··· 16 16 #include <QJsonObject> 17 17 #include <QJsonValue> 18 18 #include <QKeyEvent> 19 - #include <QDebug> 20 19 #include <QLabel> 21 20 #include <QLineEdit> 21 + #include <QGuiApplication> 22 22 #include <QListWidget> 23 23 #include <QListWidgetItem> 24 24 #include <QNetworkAccessManager> 25 25 #include <QNetworkReply> 26 26 #include <QNetworkRequest> 27 27 #include <QTimer> 28 - #include <QSet> 29 28 #include <QUrl> 29 + #include <QScreen> 30 30 #include <QUrlQuery> 31 31 #include <QStyle> 32 32 ··· 103 103 connect(m_debounce, &QTimer::timeout, this, &AddressBarController::fetchSuggestions); 104 104 connect(m_bar, &QLineEdit::textEdited, this, [this](const QString &t) { 105 105 m_pendingQuery = t.trimmed(); 106 - m_statusText = m_pendingQuery.isEmpty() ? QString() : QStringLiteral("Searching…"); 106 + m_statusText.clear(); 107 + if (m_pendingQuery.isEmpty()) { hidePopup(); return; } 107 108 populatePopup({}); 108 - if (m_pendingQuery.isEmpty()) return; 109 109 m_debounce->start(); 110 110 }); 111 111 connect(m_bar, &QLineEdit::returnPressed, this, &AddressBarController::commit); ··· 172 172 m_bar->setCursorPosition(0); 173 173 } 174 174 175 + void AddressBarController::cancelEditing() { 176 + endEditing(/*restoreUrl=*/true, m_savedUrl); 177 + if (m_bar) m_bar->clearFocus(); 178 + } 179 + 175 180 void AddressBarController::endEditing(bool restoreUrl, const QString &currentUrl) { 176 181 hidePopup(); 182 + if (m_appFilterInstalled) { 183 + qApp->removeEventFilter(this); 184 + m_appFilterInstalled = false; 185 + } 177 186 m_editing = false; 178 187 if (!currentUrl.isEmpty()) m_currentUrl = currentUrl; 179 188 if (restoreUrl && m_bar) applyDisplay(); 180 189 } 181 190 182 191 bool AddressBarController::eventFilter(QObject *obj, QEvent *ev) { 183 - if (obj != m_bar) return QObject::eventFilter(obj, ev); 192 + if (obj != m_bar) { 193 + if (m_editing && ev->type() == QEvent::MouseButtonPress) { 194 + auto *w = qobject_cast<QWidget *>(obj); 195 + if (w && w != m_bar && w != m_popup && !m_bar->isAncestorOf(w) && (!m_popup || !m_popup->isAncestorOf(w))) { 196 + endEditing(/*restoreUrl=*/true, m_savedUrl); 197 + } 198 + } 199 + return QObject::eventFilter(obj, ev); 200 + } 184 201 if (ev->type() == QEvent::FocusIn) { 185 202 beginEditing(); 186 203 } else if (ev->type() == QEvent::FocusOut) { 187 - if (m_popup && m_popup->isVisible()) return false; 188 - QWidget *now = QApplication::focusWidget(); 189 - if (!m_popup || (now != m_popup && (!m_popupList || now != m_popupList->viewport()))) { 204 + QTimer::singleShot(0, this, [this] { 205 + QWidget *now = QApplication::focusWidget(); 206 + if (now == m_bar || now == m_popup || (m_popupList && now == m_popupList->viewport())) return; 190 207 endEditing(/*restoreUrl=*/true, m_savedUrl); 191 - } 208 + }); 192 209 } else if (ev->type() == QEvent::KeyPress) { 193 210 auto *ke = static_cast<QKeyEvent *>(ev); 194 211 if (ke->key() == Qt::Key_Escape) { ··· 197 214 return true; 198 215 } 199 216 if ((ke->key() == Qt::Key_Down || ke->key() == Qt::Key_Up) && 200 - m_popupList && m_popup && m_popup->isVisible() && m_popupList->count() > 1) { 201 - // Skip the non-selectable header row at index 0. 217 + m_popupList && m_popup && m_popup->isVisible() && m_popupList->count() > 0) { 202 218 int row = m_popupList->currentRow(); 203 - if (row < 1) row = 1; 204 219 if (ke->key() == Qt::Key_Down) 205 - row = row + 1 >= m_popupList->count() ? 1 : row + 1; 220 + row = row + 1 >= m_popupList->count() ? 0 : row + 1; 206 221 else 207 - row = row <= 1 ? m_popupList->count() - 1 : row - 1; 222 + row = row <= 0 ? m_popupList->count() - 1 : row - 1; 208 223 m_popupList->setCurrentRow(row); 209 - m_bar->setText(m_popupList->item(row)->text()); 224 + m_bar->setText(m_popupList->item(row)->data(Qt::UserRole).toString().isEmpty() 225 + ? m_popupList->item(row)->text() 226 + : m_popupList->item(row)->data(Qt::UserRole).toString()); 210 227 return true; 211 228 } 212 229 } ··· 216 233 void AddressBarController::beginEditing() { 217 234 if (m_editing) return; 218 235 m_editing = true; 236 + if (!m_appFilterInstalled) { 237 + qApp->installEventFilter(this); 238 + m_appFilterInstalled = true; 239 + } 219 240 m_savedUrl = m_currentUrl; 220 241 // Swap to the full URL for editing — never the prettified form, since 221 242 // editing a domain-only string would mangle paths and queries on commit. ··· 250 271 void AddressBarController::fetchSuggestions() { 251 272 if (m_pendingQuery.isEmpty()) return; 252 273 if (m_inflight) { 253 - m_inflight->abort(); 254 - m_inflight->deleteLater(); 255 - m_inflight = nullptr; 274 + QNetworkReply *oldReply = m_inflight.data(); 275 + m_inflight.clear(); 276 + oldReply->abort(); 277 + oldReply->deleteLater(); 256 278 } 257 279 const QString engineHost = QUrl(m_searchEngine).host(); 258 280 const auto eng = engineForHost(engineHost); ··· 281 303 282 304 void AddressBarController::onSuggestionReplyFinished(QNetworkReply *reply) { 283 305 reply->deleteLater(); 284 - if (reply != m_inflight) return; 285 - m_inflight = nullptr; 306 + if (reply != m_inflight.data()) return; 307 + m_inflight.clear(); 286 308 const QString replyQuery = reply->property("query").toString(); 287 309 const bool fallbackTried = reply->property("fallbackTried").toBool(); 288 310 auto tryFallback = [this, replyQuery, fallbackTried] { ··· 316 338 }); 317 339 }; 318 340 if (reply->error() != QNetworkReply::NoError) { 319 - qDebug() << "suggestion request failed" << reply->url() << reply->errorString(); 320 341 if (fallbackTried) m_statusText = QStringLiteral("Suggestions failed: %1").arg(reply->errorString()); 321 342 tryFallback(); 322 343 return; ··· 324 345 if (!m_bar || replyQuery != m_pendingQuery) return; 325 346 326 347 const QByteArray body = reply->readAll(); 327 - qDebug() << "suggestion response" << reply->url() << body.left(240); 328 348 QJsonParseError parseError; 329 349 const auto doc = QJsonDocument::fromJson(body, &parseError); 330 350 if (parseError.error != QJsonParseError::NoError) { 331 - qDebug() << "suggestion parse failed" << parseError.errorString(); 332 351 if (fallbackTried) m_statusText = QStringLiteral("Suggestions returned non-JSON"); 333 352 tryFallback(); 334 353 return; ··· 367 386 } 368 387 } 369 388 if (items.isEmpty()) { 370 - qDebug() << "suggestion parse returned no rows" << reply->url() << body.left(240); 371 389 if (fallbackTried) m_statusText = QStringLiteral("No suggestions in response"); 372 390 tryFallback(); 373 391 return; ··· 415 433 // widget itself sits inside as a transparent child, so its rows 416 434 // can be painted/styled independently of the panel chrome. 417 435 QColor fill = m_theme.panel; 418 - fill.setAlphaF(0.96); 419 - m_popup = new AddrPopupFrame(fill, m_theme.border); 420 - m_popup->setParent(m_bar ? m_bar->window() : nullptr); 421 - m_popup->setWindowFlags(Qt::Widget); 436 + fill.setAlphaF(0.52); 437 + QColor border = m_theme.border; 438 + border.setAlpha(150); 439 + m_popup = new AddrPopupFrame(fill, border); 440 + m_popup->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::WindowStaysOnTopHint); 441 + m_popup->setAttribute(Qt::WA_ShowWithoutActivating); 442 + m_popup->setAttribute(Qt::WA_MacAlwaysShowToolWindow); 443 + m_popup->setAttribute(Qt::WA_TranslucentBackground); 444 + m_popup->setAttribute(Qt::WA_NoSystemBackground); 445 + m_popup->setAutoFillBackground(false); 422 446 m_popup->setFocusPolicy(Qt::NoFocus); 423 447 424 448 auto *vbox = new QVBoxLayout(m_popup); ··· 440 464 f.setPointSize(m_theme.regularSize); 441 465 m_popupList->setFont(f); 442 466 m_popupList->setStyleSheet(QString( 443 - "QListWidget#AddrPopup { background: transparent; border: none; padding: 0px; color: %3; }" 467 + "QListWidget#AddrPopup { background: transparent; border: none; padding: 0px; color: %1; }" 444 468 "QListWidget#AddrPopup::item {" 445 469 " padding: 6px 10px;" 446 470 " margin: 1px 2px;" 447 471 " border-radius: 6px;" 448 - " color: %3;" 472 + " color: %1;" 449 473 "}" 450 - "QListWidget#AddrPopup::item:selected { background: %4; }" 451 - "QListWidget#AddrPopup::item:hover:!selected { background: %5; }" 474 + "QListWidget#AddrPopup::item:selected { background: %2; }" 475 + "QListWidget#AddrPopup::item:hover:!selected { background: %3; }" 452 476 "QListWidget#AddrPopup QScrollBar:vertical { background: transparent; width: 6px; margin: 4px 2px; }" 453 - "QListWidget#AddrPopup QScrollBar::handle:vertical { background: %2; border-radius: 3px; min-height: 24px; }" 477 + "QListWidget#AddrPopup QScrollBar::handle:vertical { background: %4; border-radius: 3px; min-height: 24px; }" 454 478 "QListWidget#AddrPopup QScrollBar::add-line, QListWidget#AddrPopup QScrollBar::sub-line { height:0; width:0; }") 455 - .arg(m_theme.panel.name(), 456 - m_theme.border.name(), 457 - m_theme.foreground.name(), 479 + .arg(m_theme.foreground.name(), 458 480 m_theme.raised.name(), 459 - m_theme.hover.name())); 481 + m_theme.hover.name(), 482 + m_theme.border.name())); 460 483 connect(m_popupList, &QListWidget::itemClicked, this, [this](QListWidgetItem *it) { 461 484 if (!it) return; 462 485 // Skip non-selectable header row. ··· 466 489 }); 467 490 } 468 491 m_popupList->clear(); 469 - { 470 - const QString headerText = m_bar ? m_bar->text().trimmed() : QString(); 471 - const QString engineHost = QUrl(m_searchEngine).host().isEmpty() 472 - ? QStringLiteral("search") 473 - : QUrl(m_searchEngine).host(); 474 - fetchEngineIcon(engineHost); 475 - auto *header = new QListWidgetItem(headerText.isEmpty() 476 - ? QStringLiteral("Search or enter address") 477 - : QStringLiteral("Search %1 for “%2”").arg(engineHost, headerText), 478 - m_popupList); 479 - header->setData(Qt::UserRole, headerText); 480 - header->setIcon(engineIcon()); 481 - header->setFlags(headerText.isEmpty() ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable); 482 - header->setSizeHint(QSize(0, 32)); 483 - QFont f = m_popupList->font(); 484 - f.setBold(true); 485 - header->setFont(f); 492 + if (items.isEmpty()) { 493 + hidePopup(); 494 + return; 486 495 } 496 + const QString engineHost = QUrl(m_searchEngine).host().isEmpty() 497 + ? QStringLiteral("search") 498 + : QUrl(m_searchEngine).host(); 499 + fetchEngineIcon(engineHost); 487 500 for (const auto &s : items) { 488 501 auto *it = new QListWidgetItem(s, m_popupList); 489 502 it->setData(Qt::UserRole, s); 490 503 it->setIcon(engineIcon()); 491 504 it->setSizeHint(QSize(0, 30)); 492 505 } 493 - if (items.isEmpty() && !m_statusText.isEmpty()) { 494 - auto *status = new QListWidgetItem(m_statusText, m_popupList); 495 - status->setFlags(Qt::ItemIsEnabled); 496 - status->setSizeHint(QSize(0, 30)); 497 - status->setForeground(m_theme.muted); 498 - } 499 506 m_popupList->setCurrentRow(-1); 500 507 showPopup(); 501 508 } ··· 507 514 && anchor->window()->findChild<QWidget *>("WebTopbar")->isAncestorOf(anchor); 508 515 509 516 const QPoint anchorBottom = anchor->mapToGlobal(QPoint(0, anchor->height())); 510 - const QPoint parentBottom = m_popup->parentWidget() 511 - ? m_popup->parentWidget()->mapFromGlobal(anchorBottom) 512 - : anchorBottom; 513 - const QRect available = m_popup->parentWidget() 514 - ? m_popup->parentWidget()->rect() 515 - : QRect(); 517 + QRect available; 518 + if (QScreen *screen = QGuiApplication::screenAt(anchorBottom)) { 519 + available = screen->availableGeometry(); 520 + } 516 521 517 522 const int rows = qMin(m_popupList ? m_popupList->count() : 0, 9); 518 - const int height = 32 + qMax(0, rows - 1) * 30 + 12; 523 + const int height = qMax(1, rows) * 30 + 12; 519 524 int width = inTopbar ? qBound(420, anchor->width(), 720) 520 525 : qMax(anchor->width(), 320); 521 - int x = parentBottom.x() + (anchor->width() - width) / 2; 522 - int y = parentBottom.y() + (inTopbar ? 8 : 6); 526 + int x = anchorBottom.x() + (anchor->width() - width) / 2; 527 + int y = anchorBottom.y() + (inTopbar ? 8 : 6); 523 528 524 529 if (available.isValid()) { 525 530 width = qMin(width, available.width() - 24);
+4 -1
src/ui/AddressBarController.hpp
··· 5 5 #include <QColor> 6 6 #include <QIcon> 7 7 #include <QObject> 8 + #include <QPointer> 8 9 #include <QString> 9 10 10 11 class QLabel; ··· 37 38 // Cancel editing and hide popup; if restoreUrl is true, the bar's text is 38 39 // overwritten by the supplied current url string. 39 40 void endEditing(bool restoreUrl, const QString &currentUrl); 41 + void cancelEditing(); 40 42 41 43 protected: 42 44 bool eventFilter(QObject *obj, QEvent *ev) override; ··· 66 68 QString m_searchEngine; 67 69 68 70 QNetworkAccessManager *m_net = nullptr; 69 - QNetworkReply *m_inflight = nullptr; 71 + QPointer<QNetworkReply> m_inflight; 70 72 QTimer *m_debounce = nullptr; 71 73 QWidget *m_popup = nullptr; 72 74 QListWidget *m_popupList = nullptr; ··· 80 82 QIcon m_engineIcon; 81 83 QString m_engineIconHost; 82 84 bool m_engineIconLoading = false; 85 + bool m_appFilterInstalled = false; 83 86 bool m_editing = false; 84 87 85 88 void renderLock();
+1
src/web/WebView.hpp
··· 42 42 // Emitted after a navigation finishes, with the page's preferred chrome 43 43 // colour. Invalid QColor when the page exposes nothing useful. 44 44 void themeColorChanged(const QColor &color); 45 + void contentMouseDown(); 45 46 46 47 protected: 47 48 void resizeEvent(QResizeEvent *e) override;
+54
src/web/WebView.mm
··· 14 14 #import <WebKit/WKWindowFeatures.h> 15 15 #import <WebKit/WKPreferences.h> 16 16 17 + static void disableWebKit60FpsCap(WKPreferences *preferences) { 18 + if (!preferences) return; 19 + 20 + Class prefsClass = NSClassFromString(@"WKPreferences"); 21 + SEL featuresSelector = NSSelectorFromString(@"_features"); 22 + if (!prefsClass || ![prefsClass respondsToSelector:featuresSelector]) return; 23 + 24 + NSArray *features = nil; 25 + #pragma clang diagnostic push 26 + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 27 + features = [prefsClass performSelector:featuresSelector]; 28 + #pragma clang diagnostic pop 29 + if (![features isKindOfClass:[NSArray class]]) return; 30 + 31 + SEL keySelector = NSSelectorFromString(@"key"); 32 + SEL setEnabledSelector = NSSelectorFromString(@"_setEnabled:forFeature:"); 33 + if (![preferences respondsToSelector:setEnabledSelector]) return; 34 + 35 + for (id feature in features) { 36 + if (![feature respondsToSelector:keySelector]) continue; 37 + NSString *key = nil; 38 + #pragma clang diagnostic push 39 + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 40 + key = [feature performSelector:keySelector]; 41 + #pragma clang diagnostic pop 42 + if (![key isKindOfClass:[NSString class]]) continue; 43 + if (![key isEqualToString:@"PreferPageRenderingUpdatesNear60FPSEnabled"]) continue; 44 + 45 + NSMethodSignature *signature = [preferences methodSignatureForSelector:setEnabledSelector]; 46 + if (!signature) return; 47 + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; 48 + BOOL enabled = NO; 49 + invocation.target = preferences; 50 + invocation.selector = setEnabledSelector; 51 + id featureArgument = feature; 52 + [invocation setArgument:&enabled atIndex:2]; 53 + [invocation setArgument:&featureArgument atIndex:3]; 54 + [invocation invoke]; 55 + return; 56 + } 57 + } 58 + 17 59 // Forward decl of helper that does the QWidget -> NSView bridge. 18 60 static NSView *qtNSView(QWidget *w) { 19 61 if (!w) return nil; ··· 37 79 - (void)attachKVO:(WKWebView *)wk; 38 80 - (void)detachKVO:(WKWebView *)wk; 39 81 - (void)hostFrameDidChange:(NSNotification *)note; 82 + - (void)contentMouseDown:(NSGestureRecognizer *)sender; 40 83 @end 41 84 42 85 @implementation PocbWKBridge ··· 51 94 @try { [wk removeObserver:self forKeyPath:@"URL"]; } @catch (...) {} 52 95 @try { [wk removeObserver:self forKeyPath:@"title"]; } @catch (...) {} 53 96 @try { [wk removeObserver:self forKeyPath:@"estimatedProgress"]; } @catch (...) {} 97 + } 98 + 99 + - (void)contentMouseDown:(NSGestureRecognizer *)sender { 100 + (void)sender; 101 + if (self.owner) emit self.owner->contentMouseDown(); 54 102 } 55 103 56 104 - (void)observeValueForKeyPath:(NSString *)keyPath ··· 99 147 (void)wk; (void)windowFeatures; 100 148 if (!self.owner) return nil; 101 149 150 + disableWebKit60FpsCap(configuration.preferences); 102 151 WKWebView *child = [[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]; 103 152 104 153 // Wrap into a new Qt-side WebView and hand it to the parent so it can ··· 157 206 158 207 if (profile) { 159 208 WKWebViewConfiguration *cfg = [[WKWebViewConfiguration alloc] init]; 209 + disableWebKit60FpsCap(cfg.preferences); 160 210 if (profile->dataStore()) { 161 211 cfg.websiteDataStore = (__bridge WKWebsiteDataStore *)profile->dataStore(); 162 212 } ··· 192 242 [m_impl->bridge detachKVO:m_impl->wk]; 193 243 [m_impl->wk removeFromSuperview]; 194 244 } 245 + disableWebKit60FpsCap(wk.configuration.preferences); 195 246 m_impl->wk = wk; 196 247 if (!m_impl->bridge) { 197 248 m_impl->bridge = [[PocbWKBridge alloc] init]; ··· 199 250 } 200 251 wk.navigationDelegate = m_impl->bridge; 201 252 wk.UIDelegate = m_impl->bridge; 253 + NSClickGestureRecognizer *click = [[NSClickGestureRecognizer alloc] initWithTarget:m_impl->bridge action:@selector(contentMouseDown:)]; 254 + click.delaysPrimaryMouseButtonEvents = NO; 255 + [wk addGestureRecognizer:click]; 202 256 [m_impl->bridge attachKVO:wk]; 203 257 204 258 NSView *host = qtNSView(this);