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 2dd43e0c 6817bbca

+923 -33
+6
CMakeLists.txt
··· 68 68 src/services/Theme.hpp 69 69 src/services/FaviconService.cpp 70 70 src/services/FaviconService.hpp 71 + src/services/BookmarkStore.cpp 72 + src/services/BookmarkStore.hpp 71 73 src/services/ChromeExtensionManager.cpp 72 74 src/services/ChromeExtensionManager.hpp 73 75 src/ui/FloatingOmnibox.cpp ··· 84 86 src/tabs/TabTree.cpp 85 87 src/tabs/TabTree.hpp 86 88 src/mac/MacIntegration.hpp 89 + src/mac/NativeSettingsWindow.hpp 90 + src/mac/NativeProfilePopover.hpp 87 91 src/web/WebView.hpp 88 92 src/web/WebKitProfile.hpp 89 93 ) ··· 106 110 src/mac/Vibrancy.mm 107 111 src/mac/HighRefresh.mm 108 112 src/mac/UnifiedToolbar.mm 113 + src/mac/NativeSettingsWindow.mm 114 + src/mac/NativeProfilePopover.mm 109 115 src/web/WebView.mm 110 116 src/web/WebKitProfile.mm 111 117 )
+374 -30
src/app/BrowserWindow.cpp
··· 7 7 #include "ChromeExtensionManager.hpp" 8 8 #include "LayoutMetrics.hpp" 9 9 #include "MacIntegration.hpp" 10 - #include "SettingsDialog.hpp" 10 + #include "NativeProfilePopover.hpp" 11 + #include "NativeSettingsWindow.hpp" 11 12 #include "SidebarController.hpp" 12 13 #include "TabTree.hpp" 13 14 #include "Topbar.hpp" ··· 38 39 #include <QNetworkReply> 39 40 #include <QNetworkRequest> 40 41 #include <QProgressBar> 42 + #include <QPropertyAnimation> 41 43 #include <QDir> 42 44 #include <QShortcut> 43 45 #include <QShortcutEvent> ··· 53 55 #include <QToolButton> 54 56 #include <QTreeWidget> 55 57 #include <QVariantAnimation> 58 + #include <QWheelEvent> 56 59 #include <QEasingCurve> 57 60 #include <QEvent> 58 61 #include <QVBoxLayout> ··· 230 233 } 231 234 232 235 void BrowserWindow::showSettings() { 233 - SettingsDialog dialog(m_profiles, this); 234 - connect(&dialog, &SettingsDialog::homePageChanged, this, [this](const QString &url) { 235 - m_homePage = url; 236 - if (m_tabTree) m_tabTree->setHomePage(url); 237 - }); 238 - connect(&dialog, &SettingsDialog::searchEngineChanged, this, [this](const QString &url) { 239 - if (!url.contains("%1")) return; 240 - m_searchEngine = url; 241 - if (m_floatingOmnibox) m_floatingOmnibox->setSearchEngineUrl(url); 242 - }); 243 - connect(&dialog, &SettingsDialog::showFullUrlChanged, this, [this](bool full) { 244 - if (m_addressBarCtl) m_addressBarCtl->setShowFullUrl(full); 245 - }); 246 - dialog.exec(); 236 + QString homePage = m_homePage; 237 + QString searchEngine = m_searchEngine; 238 + bool showFullUrl = QSettings().value("ui/showFullUrl", false).toBool(); 239 + if (!mac::showNativeSettingsWindow(this, m_profiles, homePage, searchEngine, showFullUrl)) return; 240 + 241 + m_homePage = homePage; 242 + if (m_tabTree) m_tabTree->setHomePage(homePage); 243 + 244 + if (searchEngine.contains("%1")) { 245 + m_searchEngine = searchEngine; 246 + if (m_floatingOmnibox) m_floatingOmnibox->setSearchEngineUrl(searchEngine); 247 + } 248 + 249 + if (m_addressBarCtl) m_addressBarCtl->setShowFullUrl(showFullUrl); 247 250 } 248 251 249 252 void BrowserWindow::updateForCurrentTab() { ··· 310 313 m_floatingOmnibox->showFor(m_stack, QString()); 311 314 }; 312 315 auto settings = [this] { showSettings(); }; 316 + auto bookmark = [this] { 317 + if (auto *v = currentView()) m_bookmarks.addBookmark(m_profiles.currentName(), v->title(), v->url()); 318 + }; 313 319 if (mac::showNativePageActionsMenu(m_pillMenuBtn, copyUrl, reload, newTab, settings)) return; 314 320 315 321 QMenu menu(this); 316 322 menu.addAction("Copy URL", this, copyUrl); 317 323 menu.addAction("Reload", this, reload); 324 + menu.addAction("Bookmark This Page", this, bookmark); 318 325 menu.addSeparator(); 319 326 menu.addAction("New Tab", this, newTab); 320 327 menu.addAction("Settings…", this, settings); ··· 352 359 return w.bar; 353 360 } 354 361 362 + QWidget *BrowserWindow::buildProfileSwitcher(QWidget *parent) { 363 + auto *wrap = new QWidget(parent); 364 + wrap->setObjectName("ProfileSwitcher"); 365 + wrap->setAttribute(Qt::WA_TranslucentBackground); 366 + auto *layout = new QHBoxLayout(wrap); 367 + layout->setContentsMargins(0, 8, 0, 0); 368 + layout->setSpacing(0); 369 + 370 + m_profileBtn = new QToolButton(wrap); 371 + m_profileBtn->setObjectName("ProfileButton"); 372 + m_profileBtn->setToolButtonStyle(Qt::ToolButtonIconOnly); 373 + m_profileBtn->setCursor(Qt::PointingHandCursor); 374 + m_profileBtn->setFocusPolicy(Qt::NoFocus); 375 + m_profileBtn->setIconSize(QSize(19, 19)); 376 + m_profileBtn->setMinimumSize(32, 32); 377 + m_profileBtn->setMaximumSize(32, 32); 378 + m_profileBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); 379 + m_profileBtn->setStyleSheet(QString( 380 + "QToolButton#ProfileButton { background: transparent; border: none; border-radius: 8px; padding: 0px; color: %1; text-align: center; font-family: '%2'; font-size: %3px; }" 381 + "QToolButton#ProfileButton:hover { background: rgba(255,255,255,0.08); }" 382 + "QToolButton#ProfileButton:pressed { background: rgba(255,255,255,0.12); }") 383 + .arg(m_theme.foreground.name(), m_theme.fontFamily, QString::number(m_theme.regularSize))); 384 + layout->addWidget(m_profileBtn, 1, Qt::AlignLeft | Qt::AlignBottom); 385 + wrap->installEventFilter(this); 386 + m_profileBtn->installEventFilter(this); 387 + connect(m_profileBtn, &QToolButton::clicked, this, &BrowserWindow::showProfileMenu); 388 + updateProfileSwitcher(); 389 + return wrap; 390 + } 391 + 392 + void BrowserWindow::updateProfileSwitcher() { 393 + if (!m_profileBtn) return; 394 + const QString name = m_profiles.currentName().isEmpty() ? QStringLiteral("Default") : m_profiles.currentName(); 395 + m_profileBtn->setText(QString()); 396 + m_profileBtn->setToolTip(QStringLiteral("Profile: %1").arg(name)); 397 + m_profileBtn->setIcon(mac::sfSymbolIcon(m_profiles.iconName(name), 15.0, m_theme.foreground)); 398 + } 399 + 400 + void BrowserWindow::switchProfileRelative(int direction) { 401 + QStringList list = m_profiles.profiles(); 402 + list.removeDuplicates(); 403 + const int defaultIndex = list.indexOf("Default"); 404 + if (defaultIndex > 0) list.move(defaultIndex, 0); 405 + const int current = qMax(0, list.indexOf(m_profiles.currentName())); 406 + const int next = qBound(0, current + direction, list.size() - 1); 407 + if (next == current || next < 0 || next >= list.size()) return; 408 + animateProfileSwitcher(direction); 409 + m_profiles.setCurrentProfile(list.at(next)); 410 + } 411 + 412 + QStringList BrowserWindow::orderedProfiles() const { 413 + QStringList list = m_profiles.profiles(); 414 + list.removeDuplicates(); 415 + const int defaultIndex = list.indexOf("Default"); 416 + if (defaultIndex > 0) list.move(defaultIndex, 0); 417 + return list; 418 + } 419 + 420 + void BrowserWindow::updateCurrentProfileSnapshot() { 421 + if (!m_tabTree || !m_tabTree->widget()) return; 422 + QStringList titles; 423 + auto *tree = m_tabTree->widget(); 424 + for (int i = 0; i < tree->topLevelItemCount(); ++i) { 425 + if (auto *item = tree->topLevelItem(i)) titles.append(item->text(0).isEmpty() ? QStringLiteral("New tab") : item->text(0)); 426 + } 427 + if (titles.isEmpty()) titles.append(QStringLiteral("New tab")); 428 + m_profileTabSnapshots.insert(m_profiles.currentName(), titles); 429 + } 430 + 431 + void BrowserWindow::updateSidebarPreview(int direction) { 432 + if (!m_sidebarPreviewTabs || !m_sidebarPreviewIcon) return; 433 + const QStringList list = orderedProfiles(); 434 + const int current = qMax(0, list.indexOf(m_profiles.currentName())); 435 + const int next = qBound(0, current + direction, list.size() - 1); 436 + if (next == current || next < 0 || next >= list.size()) return; 437 + const QString profile = list.at(next); 438 + m_sidebarPreviewIcon->setIcon(mac::sfSymbolIcon(m_profiles.iconName(profile), 18.0, m_theme.foreground)); 439 + m_sidebarPreviewTabs->clear(); 440 + const QStringList titles = m_profileTabSnapshots.value(profile, QStringList{QStringLiteral("New tab")}); 441 + for (const QString &title : titles) { 442 + auto *item = new QTreeWidgetItem(QStringList() << title); 443 + item->setIcon(0, mac::sfSymbolIcon("globe", 12.0, m_theme.muted)); 444 + m_sidebarPreviewTabs->addTopLevelItem(item); 445 + } 446 + } 447 + 448 + void BrowserWindow::setSidebarSwipeOffset(int offset) { 449 + if (!m_sidebarPage || !m_sidebarViewport) return; 450 + const int width = qMax(1, m_sidebarViewport->width()); 451 + const QRect bounds = m_sidebarViewport->rect(); 452 + m_sidebarSwipeOffset = qBound(-width, offset, width); 453 + m_sidebarPage->setGeometry(bounds.translated(m_sidebarSwipeOffset, 0)); 454 + if (m_sidebarPreviewPage) { 455 + if (m_sidebarSwipeOffset == 0) { 456 + m_sidebarPreviewPage->hide(); 457 + } else { 458 + const int direction = m_sidebarSwipeOffset < 0 ? 1 : -1; 459 + updateSidebarPreview(direction); 460 + m_sidebarPreviewPage->setGeometry(bounds.translated(direction > 0 ? width + m_sidebarSwipeOffset : -width + m_sidebarSwipeOffset, 0)); 461 + m_sidebarPreviewPage->show(); 462 + } 463 + } 464 + } 465 + 466 + void BrowserWindow::settleSidebarSwipe(bool commit) { 467 + if (m_sidebarSwipeSettleTimer) m_sidebarSwipeSettleTimer->stop(); 468 + if (m_sidebarSwipeSettling) return; 469 + if (m_sidebarSwipeAnim) { 470 + m_sidebarSwipeAnim->stop(); 471 + m_sidebarSwipeAnim->deleteLater(); 472 + m_sidebarSwipeAnim = nullptr; 473 + } 474 + const int width = m_sidebarWidget ? qMax(160, m_sidebarWidget->width()) : 240; 475 + const int direction = m_sidebarSwipeOffset < 0 ? 1 : -1; 476 + const int startOffset = m_sidebarSwipeOffset; 477 + const QStringList list = orderedProfiles(); 478 + const int current = qMax(0, list.indexOf(m_profiles.currentName())); 479 + const int next = qBound(0, current + direction, list.size() - 1); 480 + if (commit && (next == current || next < 0 || next >= list.size())) commit = false; 481 + auto *driver = new QVariantAnimation(this); 482 + m_sidebarSwipeAnim = driver; 483 + driver->setStartValue(startOffset); 484 + driver->setEndValue(commit ? (direction > 0 ? -width : width) : 0); 485 + driver->setDuration(commit ? 190 : 150); 486 + driver->setEasingCurve(QEasingCurve::OutCubic); 487 + m_sidebarSwipeSettling = true; 488 + connect(driver, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) { 489 + setSidebarSwipeOffset(value.toInt()); 490 + }); 491 + connect(driver, &QVariantAnimation::finished, this, [this, driver, commit, list, next] { 492 + if (m_sidebarSwipeAnim != driver) return; 493 + if (commit && next >= 0 && next < list.size()) { 494 + m_profiles.setCurrentProfile(list.at(next)); 495 + } 496 + setSidebarSwipeOffset(0); 497 + m_sidebarSwipeActive = false; 498 + m_sidebarSwipeSettling = false; 499 + m_profileSwipeRemainder = 0; 500 + m_sidebarSwipeAnim = nullptr; 501 + driver->deleteLater(); 502 + }); 503 + driver->start(); 504 + } 505 + 506 + void BrowserWindow::animateProfileSwitcher(int direction) { 507 + if (!m_profileBtn) return; 508 + if (m_profileAnim) { 509 + m_profileAnim->stop(); 510 + m_profileAnim->deleteLater(); 511 + m_profileAnim = nullptr; 512 + } 513 + const QRect end = m_profileBtn->geometry(); 514 + QRect start = end.translated(direction > 0 ? 18 : -18, 0); 515 + m_profileBtn->setGeometry(start); 516 + m_profileAnim = new QPropertyAnimation(m_profileBtn, "geometry", this); 517 + m_profileAnim->setDuration(180); 518 + m_profileAnim->setStartValue(start); 519 + m_profileAnim->setEndValue(end); 520 + m_profileAnim->setEasingCurve(QEasingCurve::OutCubic); 521 + QPropertyAnimation *anim = m_profileAnim; 522 + connect(anim, &QPropertyAnimation::finished, anim, &QObject::deleteLater); 523 + connect(anim, &QObject::destroyed, this, [this, anim] { if (m_profileAnim == anim) m_profileAnim = nullptr; }); 524 + anim->start(); 525 + } 526 + 527 + void BrowserWindow::showProfileMenu() { 528 + if (!m_profileBtn) return; 529 + if (mac::showNativeProfilePopover(m_profileBtn, m_profiles)) return; 530 + QMenu menu(this); 531 + QStringList list = m_profiles.profiles(); 532 + list.removeDuplicates(); 533 + const int defaultIndex = list.indexOf("Default"); 534 + if (defaultIndex > 0) list.move(defaultIndex, 0); 535 + for (const QString &name : list) { 536 + auto *action = menu.addAction(mac::sfSymbolIcon(m_profiles.iconName(name), 13.0, m_theme.foreground), name); 537 + action->setCheckable(true); 538 + action->setChecked(name == m_profiles.currentName()); 539 + connect(action, &QAction::triggered, this, [this, name] { m_profiles.setCurrentProfile(name); }); 540 + } 541 + menu.addSeparator(); 542 + menu.addAction("Manage Profiles…", this, &BrowserWindow::showSettings); 543 + menu.exec(m_profileBtn->mapToGlobal(QPoint(0, m_profileBtn->height() + 2))); 544 + } 355 545 356 546 QUrl BrowserWindow::urlFromInput(const QString &input) const { 357 547 const QString trimmed = input.trimmed(); ··· 409 599 "QSplitter::handle:horizontal { background: transparent; }"); 410 600 411 601 auto *sidebar = new QWidget(m_splitter); 602 + m_sidebarWidget = sidebar; 412 603 sidebar->setObjectName("Sidebar"); 413 604 sidebar->setStyleSheet("QWidget#Sidebar { background: transparent; }"); 414 605 sidebar->setAttribute(Qt::WA_TranslucentBackground); ··· 427 618 cacheDir.mkpath("."); 428 619 m_favicons = new FaviconService(cacheDir, this); 429 620 430 - sidebar->setMinimumWidth(ui::metrics::SidebarMinimumWidth); 621 + m_addrInSidebar = QSettings().value("ui/addressBarInSidebar", false).toBool(); 622 + sidebar->setMinimumWidth(m_addrInSidebar ? ui::metrics::SidebarHeaderMinimumWidth 623 + : ui::metrics::SidebarMinimumWidth); 431 624 sidebar->setMaximumWidth(ui::metrics::SidebarMaximumWidth); 432 625 433 626 auto *stackHost = new QWidget(m_splitter); ··· 466 659 stackLayout->setContentsMargins(0, 0, 0, 0); 467 660 containerLayout->addWidget(m_stack, 1); 468 661 662 + m_sidebarViewport = new QWidget(sidebar); 663 + m_sidebarViewport->setObjectName("SidebarViewport"); 664 + m_sidebarViewport->setAttribute(Qt::WA_TranslucentBackground); 665 + m_sidebarViewport->setAttribute(Qt::WA_NativeWindow, false); 666 + m_sidebarViewport->setStyleSheet("QWidget#SidebarViewport { background: transparent; }"); 667 + m_sidebarViewport->installEventFilter(this); 668 + 669 + m_sidebarPage = new QWidget(m_sidebarViewport); 670 + m_sidebarPage->setObjectName("SidebarPage"); 671 + m_sidebarPage->setAttribute(Qt::WA_TranslucentBackground); 672 + m_sidebarPage->setStyleSheet("QWidget#SidebarPage { background: transparent; }"); 673 + auto *pageLayout = new QVBoxLayout(m_sidebarPage); 674 + pageLayout->setContentsMargins(0, 0, 0, 0); 675 + pageLayout->setSpacing(0); 676 + 469 677 // TabTree owns the sidebar list + WebView lifetime, but the QStackedLayout 470 678 // host (`m_stack`) and the surrounding sidebar chrome stay here. 471 - m_tabTree = new TabTree(m_profiles, m_favicons, m_stack, m_theme, sidebar, this); 679 + m_tabTree = new TabTree(m_profiles, m_favicons, m_stack, m_theme, m_sidebarPage, this); 472 680 m_tabTree->setHomePage(m_homePage); 473 - sideLayout->addWidget(m_tabTree->widget(), 1); 681 + pageLayout->addWidget(m_tabTree->widget(), 1); 682 + m_profileSwitcher = buildProfileSwitcher(m_sidebarPage); 683 + pageLayout->addWidget(m_profileSwitcher, 0, Qt::AlignLeft | Qt::AlignBottom); 684 + sideLayout->addWidget(m_sidebarViewport, 1); 685 + m_sidebarPreviewPage = new QWidget(m_sidebarViewport); 686 + m_sidebarPreviewPage->setObjectName("SidebarPreviewPage"); 687 + m_sidebarPreviewPage->setAttribute(Qt::WA_TranslucentBackground); 688 + m_sidebarPreviewPage->setStyleSheet("QWidget#SidebarPreviewPage { background: transparent; }"); 689 + auto *previewLayout = new QVBoxLayout(m_sidebarPreviewPage); 690 + previewLayout->setContentsMargins(0, 0, 0, 0); 691 + m_sidebarPreviewTabs = new QTreeWidget(m_sidebarPreviewPage); 692 + m_sidebarPreviewTabs->setHeaderHidden(true); 693 + m_sidebarPreviewTabs->setRootIsDecorated(false); 694 + m_sidebarPreviewTabs->setFrameShape(QFrame::NoFrame); 695 + m_sidebarPreviewTabs->setFocusPolicy(Qt::NoFocus); 696 + m_sidebarPreviewTabs->setSelectionMode(QAbstractItemView::NoSelection); 697 + m_sidebarPreviewTabs->setIconSize(QSize(16, 16)); 698 + m_sidebarPreviewTabs->setUniformRowHeights(true); 699 + m_sidebarPreviewTabs->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); 700 + m_sidebarPreviewTabs->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 701 + m_sidebarPreviewTabs->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 702 + m_sidebarPreviewTabs->setAttribute(Qt::WA_TranslucentBackground); 703 + m_sidebarPreviewTabs->viewport()->setAttribute(Qt::WA_TranslucentBackground); 704 + m_sidebarPreviewTabs->setStyleSheet(QString( 705 + "QTreeWidget { background: transparent; border: none; color: %1; outline: 0; }" 706 + "QTreeWidget::item { padding: 4px 28px 4px 6px; border: none; background: transparent; color: %1; selection-background-color: transparent; }") 707 + .arg(m_theme.foreground.name())); 708 + previewLayout->addWidget(m_sidebarPreviewTabs, 1); 709 + m_sidebarPreviewIcon = new QToolButton(m_sidebarPreviewPage); 710 + m_sidebarPreviewIcon->setToolButtonStyle(Qt::ToolButtonIconOnly); 711 + m_sidebarPreviewIcon->setIconSize(QSize(19, 19)); 712 + m_sidebarPreviewIcon->setMinimumSize(32, 32); 713 + m_sidebarPreviewIcon->setMaximumSize(32, 32); 714 + m_sidebarPreviewIcon->setEnabled(false); 715 + m_sidebarPreviewIcon->setStyleSheet("QToolButton { background: transparent; border: none; }"); 716 + previewLayout->addWidget(m_sidebarPreviewIcon, 0, Qt::AlignLeft | Qt::AlignBottom); 717 + m_sidebarPreviewPage->hide(); 718 + m_sidebarPage->setGeometry(m_sidebarViewport->rect()); 719 + m_sidebarPreviewPage->setGeometry(m_sidebarViewport->rect().translated(m_sidebarViewport->width(), 0)); 474 720 475 721 hostLayout->addWidget(m_webContainer, 1); 476 722 m_splitter->addWidget(sidebar); ··· 490 736 hostLayout->setContentsMargins(ui::metrics::stackHostMargins(sidebarVisible)); 491 737 }; 492 738 m_sidebar = new SidebarController(this, m_splitter, applyStackHostInset, this); 493 - m_sidebar->setSidebarContent(m_tabTree->widget(), sideLayout); 739 + m_sidebar->setSidebarContent(m_sidebarViewport, sideLayout); 740 + m_sidebarSwipeSettleTimer = new QTimer(this); 741 + m_sidebarSwipeSettleTimer->setSingleShot(true); 742 + m_sidebarSwipeSettleTimer->setInterval(260); 743 + connect(m_sidebarSwipeSettleTimer, &QTimer::timeout, this, [this] { 744 + if (!m_sidebarSwipeActive) return; 745 + settleSidebarSwipe(qAbs(m_profileSwipeRemainder) >= qMax(160, m_sidebarWidget ? m_sidebarWidget->width() : 240) / 3); 746 + }); 494 747 495 - m_addrInSidebar = QSettings().value("ui/addressBarInSidebar", false).toBool(); 496 748 if (m_addrInSidebar) { 497 749 // Drop the toolbar entirely; the address pill + nav buttons live in 498 750 // the sidebar instead. ··· 510 762 ui::metrics::DockedSidebarRightInset, 511 763 ui::metrics::DockedSidebarBottomInset); 512 764 513 - m_sidebarHeader = new QWidget(sidebar); 765 + m_sidebarHeader = new QWidget(m_sidebarPage); 514 766 m_sidebarHeader->setObjectName("SidebarHeader"); 515 767 m_sidebarHeader->setStyleSheet("QWidget#SidebarHeader { background: transparent; }"); 516 768 auto *headerCol = new QVBoxLayout(m_sidebarHeader); ··· 571 823 headerCol->addWidget(m_addrWrap); 572 824 } 573 825 574 - sideLayout->insertWidget(0, m_sidebarHeader); 826 + pageLayout->insertWidget(0, m_sidebarHeader); 575 827 } 576 828 577 829 connect(m_splitter, &QSplitter::splitterMoved, this, [this, sidebar](int pos, int) { ··· 595 847 central->setStyleSheet("QWidget#CentralRoot { background: transparent; }"); 596 848 central->setAttribute(Qt::WA_TranslucentBackground); 597 849 setContentsMargins(0, 0, 0, 0); 850 + qApp->installEventFilter(this); 598 851 599 852 if (auto *sb = statusBar()) sb->hide(); 600 853 setStatusBar(nullptr); 601 854 602 855 connect(m_omnibox, &QLineEdit::returnPressed, this, &BrowserWindow::loadFromOmnibox); 603 - connect(m_tabTree, &TabTree::currentTabChanged, this, &BrowserWindow::updateForCurrentTab); 856 + connect(m_tabTree, &TabTree::currentTabChanged, this, [this] { 857 + updateForCurrentTab(); 858 + updateCurrentProfileSnapshot(); 859 + }); 604 860 connect(m_tabTree, &TabTree::loadProgress, this, [this](int progress) { 605 861 if (auto *pill = qobject_cast<ui::AddrPill *>(m_addrWrap)) { 606 862 pill->setLoadProgress(progress); ··· 613 869 }); 614 870 connect(&m_profiles, &ProfileStore::currentProfileChanged, this, [this] { 615 871 m_tabTree->rebuildForProfile(); 872 + updateProfileSwitcher(); 873 + }); 874 + connect(&m_profiles, &ProfileStore::profilesChanged, this, [this] { 875 + updateProfileSwitcher(); 616 876 }); 617 877 } 618 878 ··· 764 1024 765 1025 // ── Bookmarks (placeholders — bookmark store not yet implemented) ── 766 1026 auto *bookmarksMenu = mb->addMenu("Bookmarks"); 767 - auto *bookmarkPage = makeAction("Bookmark This Page…", QKeySequence(Qt::CTRL | Qt::Key_D), nullptr); 768 - bookmarkPage->setEnabled(false); 769 - bookmarksMenu->addAction(bookmarkPage); 770 - auto *showBookmarks = makeAction("Show All Bookmarks", 771 - QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_B), nullptr); 772 - showBookmarks->setEnabled(false); 773 - bookmarksMenu->addAction(showBookmarks); 1027 + auto addCurrentBookmark = [this] { 1028 + if (auto *v = currentView()) m_bookmarks.addBookmark(m_profiles.currentName(), v->title(), v->url()); 1029 + }; 1030 + auto rebuildBookmarksMenu = [this, bookmarksMenu, addCurrentBookmark] { 1031 + bookmarksMenu->clear(); 1032 + auto *bookmarkPage = bookmarksMenu->addAction("Bookmark This Page"); 1033 + bookmarkPage->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D)); 1034 + connect(bookmarkPage, &QAction::triggered, this, addCurrentBookmark); 1035 + if (auto *v = currentView()) { 1036 + bookmarkPage->setEnabled(v->url().isValid() && !v->url().isEmpty() && v->url().scheme() != "about" && v->url().scheme() != "data"); 1037 + } else { 1038 + bookmarkPage->setEnabled(false); 1039 + } 1040 + bookmarksMenu->addSeparator(); 1041 + const QVector<Bookmark> items = m_bookmarks.bookmarks(m_profiles.currentName()); 1042 + if (items.isEmpty()) { 1043 + auto *empty = bookmarksMenu->addAction("No Bookmarks"); 1044 + empty->setEnabled(false); 1045 + } else { 1046 + for (const Bookmark &bookmark : items) { 1047 + auto *open = bookmarksMenu->addAction(bookmark.title); 1048 + open->setToolTip(bookmark.url.toString()); 1049 + connect(open, &QAction::triggered, this, [this, url = bookmark.url] { 1050 + if (auto *v = currentView()) v->load(url); 1051 + }); 1052 + auto *remove = bookmarksMenu->addAction(QString("Remove “%1”").arg(bookmark.title)); 1053 + connect(remove, &QAction::triggered, this, [this, url = bookmark.url] { 1054 + m_bookmarks.removeBookmark(m_profiles.currentName(), url); 1055 + }); 1056 + } 1057 + } 1058 + }; 1059 + connect(bookmarksMenu, &QMenu::aboutToShow, this, rebuildBookmarksMenu); 1060 + connect(&m_bookmarks, &BookmarkStore::bookmarksChanged, this, [this, rebuildBookmarksMenu](const QString &profileName) { 1061 + if (profileName == m_profiles.currentName()) rebuildBookmarksMenu(); 1062 + }); 1063 + connect(&m_profiles, &ProfileStore::currentProfileChanged, this, rebuildBookmarksMenu); 1064 + rebuildBookmarksMenu(); 1065 + auto *bookmarkPageShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_D), this); 1066 + bookmarkPageShortcut->setContext(Qt::ApplicationShortcut); 1067 + connect(bookmarkPageShortcut, &QShortcut::activated, this, addCurrentBookmark); 774 1068 775 1069 // ── History ──────────────────────────────────────────────────────── 776 1070 auto *historyMenu = mb->addMenu("History"); ··· 834 1128 835 1129 836 1130 bool BrowserWindow::eventFilter(QObject *obj, QEvent *ev) { 1131 + if (obj == m_sidebarViewport && ev->type() == QEvent::Resize) { 1132 + setSidebarSwipeOffset(0); 1133 + if (m_sidebarPreviewTabs) m_sidebarPreviewTabs->setColumnWidth(0, m_sidebarViewport->width()); 1134 + } 1135 + if (ev->type() == QEvent::Wheel && m_sidebarWidget && m_sidebarWidget->isVisible() && !m_sidebarSwipeSettling) { 1136 + const QPoint global = QCursor::pos(); 1137 + const QRect sidebarRect(m_sidebarWidget->mapToGlobal(QPoint(0, 0)), m_sidebarWidget->size()); 1138 + if (sidebarRect.contains(global)) { 1139 + auto *wheel = static_cast<QWheelEvent *>(ev); 1140 + const QPoint pixel = wheel->pixelDelta(); 1141 + const QPoint angle = wheel->angleDelta(); 1142 + const int horizontal = pixel.x() != 0 ? pixel.x() : angle.x() / 2; 1143 + const int vertical = pixel.y() != 0 ? pixel.y() : angle.y() / 2; 1144 + if (qAbs(horizontal) > qAbs(vertical) && horizontal != 0) { 1145 + if (m_sidebarSwipeSettleTimer) m_sidebarSwipeSettleTimer->stop(); 1146 + const int width = qMax(160, m_sidebarWidget->width()); 1147 + const int intended = m_profileSwipeRemainder + horizontal; 1148 + const int intendedDirection = intended < 0 ? 1 : -1; 1149 + const QStringList list = orderedProfiles(); 1150 + const int profileIndex = qMax(0, list.indexOf(m_profiles.currentName())); 1151 + const int targetIndex = profileIndex + intendedDirection; 1152 + if (targetIndex < 0 || targetIndex >= list.size()) { 1153 + m_profileSwipeRemainder = 0; 1154 + setSidebarSwipeOffset(0); 1155 + m_sidebarSwipeActive = false; 1156 + return true; 1157 + } 1158 + m_profileSwipeRemainder = qBound(-width, intended, width); 1159 + const int sign = m_profileSwipeRemainder < 0 ? -1 : 1; 1160 + const int magnitude = qAbs(m_profileSwipeRemainder); 1161 + const int displayed = magnitude <= width * 2 / 3 1162 + ? magnitude 1163 + : width * 2 / 3 + (magnitude - width * 2 / 3) / 4; 1164 + setSidebarSwipeOffset(sign * displayed); 1165 + m_sidebarSwipeActive = true; 1166 + if (magnitude >= width * 3 / 4) { 1167 + settleSidebarSwipe(true); 1168 + } else if (wheel->phase() == Qt::ScrollEnd) { 1169 + settleSidebarSwipe(magnitude >= width / 4); 1170 + } else if (wheel->phase() == Qt::NoScrollPhase && m_sidebarSwipeSettleTimer) { 1171 + m_sidebarSwipeSettleTimer->start(); 1172 + } 1173 + return true; 1174 + } 1175 + if (m_sidebarSwipeActive && wheel->phase() == Qt::ScrollEnd) { 1176 + settleSidebarSwipe(qAbs(m_profileSwipeRemainder) >= qMax(160, m_sidebarWidget->width()) / 3); 1177 + return true; 1178 + } 1179 + } 1180 + } 837 1181 return QMainWindow::eventFilter(obj, ev); 838 1182 } 839 1183
+32
src/app/BrowserWindow.hpp
··· 1 1 #pragma once 2 2 3 + #include "BookmarkStore.hpp" 3 4 #include "FaviconService.hpp" 4 5 #include "ProfileStore.hpp" 5 6 #include "Theme.hpp" 6 7 7 8 #include <QHash> 8 9 #include <QList> 10 + #include <QStringList> 9 11 #include <QMainWindow> 10 12 #include <QUrl> 11 13 #include <functional> ··· 21 23 class QNetworkAccessManager; 22 24 class QNetworkReply; 23 25 class QProgressBar; 26 + class QPropertyAnimation; 24 27 class QTimer; 28 + class QVariantAnimation; 25 29 class QSplitter; 26 30 class QToolBar; 27 31 class QToolButton; 28 32 class QAction; 29 33 class QTreeWidget; 34 + class QTreeWidget; 30 35 class QTreeWidgetItem; 31 36 class WebView; 32 37 ··· 59 64 void setupUi(); 60 65 void setupActions(); 61 66 QWidget *buildTopbar(QWidget *parent); 67 + QWidget *buildProfileSwitcher(QWidget *parent); 68 + void updateProfileSwitcher(); 69 + void switchProfileRelative(int direction); 70 + void animateProfileSwitcher(int direction); 71 + void setSidebarSwipeOffset(int offset); 72 + void settleSidebarSwipe(bool commit); 73 + QStringList orderedProfiles() const; 74 + void updateCurrentProfileSnapshot(); 75 + void updateSidebarPreview(int direction); 76 + void showProfileMenu(); 62 77 void showCopiedLinkPopup(); 63 78 Theme m_theme; 64 79 ProfileStore m_profiles; 80 + BookmarkStore m_bookmarks; 65 81 QString m_homePage = "https://search.brave.com"; 66 82 QString m_searchEngine = "https://search.brave.com/search?q=%1"; 67 83 ··· 79 95 QToolButton *m_reloadBtn = nullptr; 80 96 QToolButton *m_settingsBtn = nullptr; 81 97 QToolButton *m_newTabBtn = nullptr; 98 + QToolButton *m_profileBtn = nullptr; 99 + QWidget *m_profileSwitcher = nullptr; 100 + QPropertyAnimation *m_profileAnim = nullptr; 101 + QVariantAnimation *m_sidebarSwipeAnim = nullptr; 102 + QTimer *m_sidebarSwipeSettleTimer = nullptr; 103 + int m_profileSwipeRemainder = 0; 104 + int m_sidebarSwipeOffset = 0; 105 + bool m_sidebarSwipeActive = false; 106 + bool m_sidebarSwipeSettling = false; 107 + QHash<QString, QStringList> m_profileTabSnapshots; 82 108 QHash<QString, QToolButton *> m_extensionActionButtons; 83 109 QLineEdit *m_addressBar = nullptr; 84 110 QLabel *m_lockIcon = nullptr; 85 111 QLabel *m_searchIcon = nullptr; 86 112 QToolButton *m_pillMenuBtn = nullptr; 87 113 QWidget *m_addrWrap = nullptr; 114 + QWidget *m_sidebarWidget = nullptr; 115 + QWidget *m_sidebarViewport = nullptr; 116 + QWidget *m_sidebarPage = nullptr; 117 + QWidget *m_sidebarPreviewPage = nullptr; 118 + QTreeWidget *m_sidebarPreviewTabs = nullptr; 119 + QToolButton *m_sidebarPreviewIcon = nullptr; 88 120 QWidget *m_sidebarHeader = nullptr; 89 121 bool m_addrInSidebar = false; 90 122 QColor m_lastAppliedChrome;
+10
src/mac/NativeProfilePopover.hpp
··· 1 + #pragma once 2 + 3 + class ProfileStore; 4 + class QWidget; 5 + 6 + namespace mac { 7 + 8 + bool showNativeProfilePopover(QWidget *anchor, ProfileStore &profiles); 9 + 10 + } // namespace mac
+224
src/mac/NativeProfilePopover.mm
··· 1 + #include "NativeProfilePopover.hpp" 2 + 3 + #include "ProfileStore.hpp" 4 + 5 + #include <QStringList> 6 + #include <QWidget> 7 + 8 + #import <AppKit/AppKit.h> 9 + 10 + namespace { 11 + 12 + NSString *toNSString(const QString &value) { 13 + return [NSString stringWithUTF8String:value.toUtf8().constData()]; 14 + } 15 + 16 + QString toQString(NSString *value) { 17 + if (!value) return QString(); 18 + return QString::fromUtf8([value UTF8String]); 19 + } 20 + 21 + NSImage *symbolImage(NSString *name) { 22 + if (@available(macOS 11.0, *)) { 23 + NSImage *image = [NSImage imageWithSystemSymbolName:name accessibilityDescription:nil]; 24 + if (image) return image; 25 + return [NSImage imageWithSystemSymbolName:@"questionmark.circle" accessibilityDescription:nil]; 26 + } 27 + return nil; 28 + } 29 + 30 + NSButton *symbolButton(NSString *name, BOOL selected) { 31 + NSButton *button = [NSButton buttonWithImage:symbolImage(name) target:nil action:nil]; 32 + button.translatesAutoresizingMaskIntoConstraints = NO; 33 + button.bezelStyle = selected ? NSBezelStyleRegularSquare : NSBezelStyleInline; 34 + button.imagePosition = NSImageOnly; 35 + button.controlSize = NSControlSizeLarge; 36 + button.toolTip = name; 37 + button.wantsLayer = YES; 38 + button.layer.cornerRadius = 9.0; 39 + [button.widthAnchor constraintEqualToConstant:34.0].active = YES; 40 + [button.heightAnchor constraintEqualToConstant:34.0].active = YES; 41 + return button; 42 + } 43 + 44 + } // namespace 45 + 46 + @interface ProfilePopoverController : NSViewController 47 + @property(nonatomic, assign) ProfileStore *profiles; 48 + @property(nonatomic, strong) NSPopover *popover; 49 + @property(nonatomic, strong) NSTextField *nameField; 50 + @property(nonatomic, strong) NSMutableArray<NSButton *> *iconButtons; 51 + @property(nonatomic, copy) NSString *selectedIcon; 52 + @property(nonatomic, assign) BOOL creatingNew; 53 + @end 54 + 55 + @implementation ProfilePopoverController 56 + 57 + - (void)loadView { 58 + NSView *root = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 292, 330)]; 59 + root.translatesAutoresizingMaskIntoConstraints = NO; 60 + 61 + NSStackView *stack = [[NSStackView alloc] init]; 62 + stack.orientation = NSUserInterfaceLayoutOrientationVertical; 63 + stack.spacing = 10.0; 64 + stack.edgeInsets = NSEdgeInsetsMake(14, 14, 14, 14); 65 + stack.translatesAutoresizingMaskIntoConstraints = NO; 66 + [root addSubview:stack]; 67 + [NSLayoutConstraint activateConstraints:@[ 68 + [stack.leadingAnchor constraintEqualToAnchor:root.leadingAnchor], 69 + [stack.trailingAnchor constraintEqualToAnchor:root.trailingAnchor], 70 + [stack.topAnchor constraintEqualToAnchor:root.topAnchor], 71 + [stack.bottomAnchor constraintEqualToAnchor:root.bottomAnchor] 72 + ]]; 73 + 74 + self.nameField = [NSTextField textFieldWithString:toNSString(self.profiles->currentName())]; 75 + self.nameField.placeholderString = @"Profile name"; 76 + self.nameField.controlSize = NSControlSizeRegular; 77 + self.nameField.font = [NSFont systemFontOfSize:13.0]; 78 + self.nameField.translatesAutoresizingMaskIntoConstraints = NO; 79 + [self.nameField.heightAnchor constraintEqualToConstant:24.0].active = YES; 80 + [stack addArrangedSubview:self.nameField]; 81 + 82 + NSScrollView *scroll = [[NSScrollView alloc] init]; 83 + scroll.translatesAutoresizingMaskIntoConstraints = NO; 84 + scroll.hasVerticalScroller = NO; 85 + scroll.hasHorizontalScroller = NO; 86 + scroll.autohidesScrollers = YES; 87 + scroll.borderType = NSNoBorder; 88 + scroll.drawsBackground = NO; 89 + [scroll.heightAnchor constraintEqualToConstant:226.0].active = YES; 90 + [stack addArrangedSubview:scroll]; 91 + 92 + NSGridView *grid = [[NSGridView alloc] init]; 93 + grid.translatesAutoresizingMaskIntoConstraints = NO; 94 + grid.rowSpacing = 8.0; 95 + grid.columnSpacing = 8.0; 96 + scroll.documentView = grid; 97 + 98 + NSArray<NSString *> *icons = @[ 99 + @"person.crop.circle.fill", @"person.fill", @"person.2.fill", @"globe", @"briefcase.fill", @"house.fill", 100 + @"sparkles", @"bolt.fill", @"moon.fill", @"sun.max.fill", @"cloud.fill", @"flame.fill", 101 + @"heart.fill", @"star.fill", @"flag.fill", @"bookmark.fill", @"tag.fill", @"bell.fill", 102 + @"gamecontroller.fill", @"paintbrush.fill", @"pencil", @"book.fill", @"graduationcap.fill", @"terminal", 103 + @"lock.fill", @"key.fill", @"shield.fill", @"eye.fill", @"camera.fill", @"photo.fill", 104 + @"music.note", @"headphones", @"mic.fill", @"film.fill", @"tv.fill", @"display", 105 + @"airplane", @"car.fill", @"bicycle", @"leaf.fill", @"hare.fill", @"tortoise.fill", 106 + @"cup.and.saucer.fill", @"fork.knife", @"gift.fill", @"cart.fill", @"creditcard.fill", @"banknote.fill", 107 + @"hammer.fill", @"wrench.and.screwdriver.fill", @"gearshape.fill", @"cpu.fill", @"memorychip.fill", @"network", 108 + @"paperplane.fill", @"message.fill", @"bubble.left.fill", @"envelope.fill", @"calendar", @"clock.fill" 109 + ]; 110 + self.selectedIcon = toNSString(self.profiles->iconName(self.profiles->currentName())); 111 + self.iconButtons = [NSMutableArray array]; 112 + const NSInteger columns = 6; 113 + const NSInteger rows = ((NSInteger)icons.count + columns - 1) / columns; 114 + for (NSInteger row = 0; row < rows; ++row) { 115 + NSMutableArray<NSView *> *views = [NSMutableArray array]; 116 + for (NSInteger col = 0; col < columns; ++col) { 117 + NSInteger index = row * columns + col; 118 + if (index >= (NSInteger)icons.count) { 119 + NSView *spacer = [[NSView alloc] init]; 120 + [spacer.widthAnchor constraintEqualToConstant:34.0].active = YES; 121 + [views addObject:spacer]; 122 + continue; 123 + } 124 + NSString *icon = icons[index]; 125 + NSButton *button = symbolButton(icon, [icon isEqualToString:self.selectedIcon]); 126 + button.target = self; 127 + button.action = @selector(iconPicked:); 128 + button.identifier = icon; 129 + [self.iconButtons addObject:button]; 130 + [views addObject:button]; 131 + } 132 + [grid addRowWithViews:views]; 133 + } 134 + [grid.widthAnchor constraintGreaterThanOrEqualToConstant:244.0].active = YES; 135 + 136 + NSStackView *buttons = [[NSStackView alloc] init]; 137 + buttons.orientation = NSUserInterfaceLayoutOrientationHorizontal; 138 + buttons.spacing = 8.0; 139 + buttons.alignment = NSLayoutAttributeCenterY; 140 + buttons.translatesAutoresizingMaskIntoConstraints = NO; 141 + NSButton *add = [NSButton buttonWithImage:symbolImage(@"plus") target:self action:@selector(addProfile:)]; 142 + add.bezelStyle = NSBezelStyleRounded; 143 + add.controlSize = NSControlSizeLarge; 144 + add.toolTip = @"New Profile"; 145 + [add.widthAnchor constraintEqualToConstant:32.0].active = YES; 146 + NSButton *cancel = [NSButton buttonWithTitle:@"Cancel" target:self action:@selector(cancel:)]; 147 + cancel.bezelStyle = NSBezelStyleRounded; 148 + cancel.controlSize = NSControlSizeLarge; 149 + NSButton *save = [NSButton buttonWithTitle:@"Save" target:self action:@selector(save:)]; 150 + save.bezelStyle = NSBezelStyleRounded; 151 + save.controlSize = NSControlSizeLarge; 152 + save.keyEquivalent = @"\r"; 153 + [buttons addArrangedSubview:add]; 154 + [buttons addArrangedSubview:[[NSView alloc] init]]; 155 + [buttons addArrangedSubview:cancel]; 156 + [buttons addArrangedSubview:save]; 157 + [buttons setHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal]; 158 + [stack addArrangedSubview:buttons]; 159 + 160 + self.view = root; 161 + } 162 + 163 + - (void)iconPicked:(NSButton *)sender { 164 + self.selectedIcon = sender.identifier; 165 + for (NSButton *button in self.iconButtons) { 166 + button.bezelStyle = [button.identifier isEqualToString:self.selectedIcon] ? NSBezelStyleRegularSquare : NSBezelStyleInline; 167 + } 168 + } 169 + 170 + - (void)cancel:(id)sender { 171 + [self.popover close]; 172 + } 173 + 174 + - (void)addProfile:(id)sender { 175 + self.creatingNew = YES; 176 + self.nameField.stringValue = @""; 177 + self.nameField.placeholderString = @"New profile name"; 178 + self.selectedIcon = @"person.crop.circle.fill"; 179 + for (NSButton *button in self.iconButtons) { 180 + button.bezelStyle = [button.identifier isEqualToString:self.selectedIcon] ? NSBezelStyleRegularSquare : NSBezelStyleInline; 181 + } 182 + [self.view.window makeFirstResponder:self.nameField]; 183 + } 184 + 185 + - (void)save:(id)sender { 186 + QString name = toQString(self.nameField.stringValue).trimmed(); 187 + if (self.creatingNew) { 188 + if (name.isEmpty()) return; 189 + self.profiles->createProfile(name); 190 + self.profiles->setCurrentProfile(name); 191 + self.profiles->setIconName(name, toQString(self.selectedIcon)); 192 + [self.popover close]; 193 + return; 194 + } 195 + QString oldName = self.profiles->currentName(); 196 + if (!name.isEmpty() && name != oldName) self.profiles->renameProfile(oldName, name); 197 + self.profiles->setIconName(self.profiles->currentName(), toQString(self.selectedIcon)); 198 + [self.popover close]; 199 + } 200 + 201 + @end 202 + 203 + namespace mac { 204 + 205 + bool showNativeProfilePopover(QWidget *anchor, ProfileStore &profiles) { 206 + if (!anchor) return false; 207 + NSView *view = (__bridge NSView *)(reinterpret_cast<void *>(anchor->winId())); 208 + if (!view) return false; 209 + 210 + NSPopover *popover = [[NSPopover alloc] init]; 211 + popover.behavior = NSPopoverBehaviorTransient; 212 + popover.animates = YES; 213 + if (@available(macOS 10.10, *)) popover.appearance = NSAppearance.currentDrawingAppearance; 214 + 215 + ProfilePopoverController *controller = [[ProfilePopoverController alloc] init]; 216 + controller.profiles = &profiles; 217 + controller.popover = popover; 218 + popover.contentViewController = controller; 219 + popover.contentSize = NSMakeSize(292, 330); 220 + [popover showRelativeToRect:view.bounds ofView:view preferredEdge:NSRectEdgeMaxY]; 221 + return true; 222 + } 223 + 224 + } // namespace mac
+16
src/mac/NativeSettingsWindow.hpp
··· 1 + #pragma once 2 + 3 + #include <QString> 4 + 5 + class ProfileStore; 6 + class QWidget; 7 + 8 + namespace mac { 9 + 10 + bool showNativeSettingsWindow(QWidget *parent, 11 + ProfileStore &profiles, 12 + QString &homePage, 13 + QString &searchEngine, 14 + bool &showFullUrl); 15 + 16 + } // namespace mac
+81
src/mac/NativeSettingsWindow.mm
··· 1 + #include "NativeSettingsWindow.hpp" 2 + 3 + #include "ProfileStore.hpp" 4 + 5 + #include <QWidget> 6 + 7 + #import <AppKit/AppKit.h> 8 + 9 + namespace { 10 + 11 + NSWindow *parentWindow(QWidget *parent) { 12 + if (!parent) return NSApp.keyWindow; 13 + NSView *view = (__bridge NSView *)(reinterpret_cast<void *>(parent->winId())); 14 + return view.window ?: NSApp.keyWindow; 15 + } 16 + 17 + void positionWindow(NSWindow *window, NSWindow *owner) { 18 + if (!owner) { 19 + [window center]; 20 + return; 21 + } 22 + NSRect ownerFrame = owner.frame; 23 + NSRect frame = window.frame; 24 + frame.origin.x = NSMidX(ownerFrame) - frame.size.width / 2.0; 25 + frame.origin.y = NSMidY(ownerFrame) - frame.size.height / 2.0; 26 + [window setFrameOrigin:frame.origin]; 27 + } 28 + 29 + } // namespace 30 + 31 + namespace mac { 32 + 33 + bool showNativeSettingsWindow(QWidget *parent, 34 + ProfileStore &profiles, 35 + QString &homePage, 36 + QString &searchEngine, 37 + bool &showFullUrl) { 38 + Q_UNUSED(profiles); 39 + Q_UNUSED(homePage); 40 + Q_UNUSED(searchEngine); 41 + Q_UNUSED(showFullUrl); 42 + 43 + @autoreleasepool { 44 + NSWindow *window = [[NSWindow alloc] 45 + initWithContentRect:NSMakeRect(0, 0, 640, 520) 46 + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView 47 + backing:NSBackingStoreBuffered 48 + defer:NO]; 49 + window.title = @"Settings"; 50 + window.titlebarAppearsTransparent = YES; 51 + window.movableByWindowBackground = YES; 52 + window.releasedWhenClosed = NO; 53 + window.minSize = NSMakeSize(480, 320); 54 + 55 + NSVisualEffectView *root = [[NSVisualEffectView alloc] init]; 56 + root.material = NSVisualEffectMaterialUnderWindowBackground; 57 + root.blendingMode = NSVisualEffectBlendingModeBehindWindow; 58 + root.state = NSVisualEffectStateActive; 59 + window.contentView = root; 60 + 61 + __block bool closed = false; 62 + id observer = [[NSNotificationCenter defaultCenter] 63 + addObserverForName:NSWindowWillCloseNotification 64 + object:window 65 + queue:nil 66 + usingBlock:^(NSNotification *) { 67 + closed = true; 68 + [NSApp stopModal]; 69 + }]; 70 + 71 + positionWindow(window, parentWindow(parent)); 72 + [window makeKeyAndOrderFront:nil]; 73 + [NSApp runModalForWindow:window]; 74 + 75 + [[NSNotificationCenter defaultCenter] removeObserver:observer]; 76 + if (!closed) [window close]; 77 + return false; 78 + } 79 + } 80 + 81 + } // namespace mac
+102
src/services/BookmarkStore.cpp
··· 1 + #include "BookmarkStore.hpp" 2 + 3 + #include <QDir> 4 + #include <QFile> 5 + #include <QFileInfo> 6 + #include <QJsonArray> 7 + #include <QJsonDocument> 8 + #include <QJsonObject> 9 + #include <QSaveFile> 10 + #include <QStandardPaths> 11 + 12 + #include <algorithm> 13 + 14 + BookmarkStore::BookmarkStore(QObject *parent) : QObject(parent) { 15 + } 16 + 17 + QVector<Bookmark> BookmarkStore::bookmarks(const QString &profileName) const { 18 + return read(profileName); 19 + } 20 + 21 + bool BookmarkStore::contains(const QString &profileName, const QUrl &url) const { 22 + const QString target = url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash).toString(); 23 + for (const Bookmark &bookmark : read(profileName)) { 24 + if (bookmark.url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash).toString() == target) return true; 25 + } 26 + return false; 27 + } 28 + 29 + void BookmarkStore::addBookmark(const QString &profileName, const QString &title, const QUrl &url) { 30 + if (!url.isValid() || url.isEmpty() || url.scheme() == "about" || url.scheme() == "data") return; 31 + QVector<Bookmark> items = read(profileName); 32 + const QString target = url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash).toString(); 33 + for (Bookmark &bookmark : items) { 34 + if (bookmark.url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash).toString() == target) { 35 + bookmark.title = title.trimmed().isEmpty() ? url.toString() : title.trimmed(); 36 + bookmark.url = url; 37 + writeBookmarks(profileName, items); 38 + emit bookmarksChanged(profileName); 39 + return; 40 + } 41 + } 42 + items.append({title.trimmed().isEmpty() ? url.toString() : title.trimmed(), url}); 43 + writeBookmarks(profileName, items); 44 + emit bookmarksChanged(profileName); 45 + } 46 + 47 + void BookmarkStore::removeBookmark(const QString &profileName, const QUrl &url) { 48 + QVector<Bookmark> items = read(profileName); 49 + const QString target = url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash).toString(); 50 + const qsizetype before = items.size(); 51 + items.erase(std::remove_if(items.begin(), items.end(), [&](const Bookmark &bookmark) { 52 + return bookmark.url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash).toString() == target; 53 + }), items.end()); 54 + if (items.size() == before) return; 55 + writeBookmarks(profileName, items); 56 + emit bookmarksChanged(profileName); 57 + } 58 + 59 + QString BookmarkStore::sanitize(const QString &name) const { 60 + QString out = name.trimmed(); 61 + out.replace('/', '-'); 62 + out.replace(':', '-'); 63 + return out.isEmpty() ? QStringLiteral("Default") : out; 64 + } 65 + 66 + QString BookmarkStore::filePath(const QString &profileName) const { 67 + const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); 68 + const QDir dir(base + "/Profiles/" + sanitize(profileName)); 69 + return dir.filePath("bookmarks.json"); 70 + } 71 + 72 + QVector<Bookmark> BookmarkStore::read(const QString &profileName) const { 73 + QFile file(filePath(profileName)); 74 + if (!file.open(QIODevice::ReadOnly)) return {}; 75 + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); 76 + if (!doc.isArray()) return {}; 77 + QVector<Bookmark> items; 78 + for (const QJsonValue &value : doc.array()) { 79 + const QJsonObject object = value.toObject(); 80 + const QUrl url(object.value("url").toString()); 81 + if (!url.isValid() || url.isEmpty()) continue; 82 + const QString title = object.value("title").toString(url.toString()).trimmed(); 83 + items.append({title.isEmpty() ? url.toString() : title, url}); 84 + } 85 + return items; 86 + } 87 + 88 + void BookmarkStore::writeBookmarks(const QString &profileName, const QVector<Bookmark> &bookmarks) const { 89 + const QString path = filePath(profileName); 90 + QDir().mkpath(QFileInfo(path).absolutePath()); 91 + QJsonArray array; 92 + for (const Bookmark &bookmark : bookmarks) { 93 + QJsonObject object; 94 + object.insert("title", bookmark.title); 95 + object.insert("url", bookmark.url.toString()); 96 + array.append(object); 97 + } 98 + QSaveFile file(path); 99 + if (!file.open(QIODevice::WriteOnly)) return; 100 + file.write(QJsonDocument(array).toJson(QJsonDocument::Indented)); 101 + file.commit(); 102 + }
+32
src/services/BookmarkStore.hpp
··· 1 + #pragma once 2 + 3 + #include <QObject> 4 + #include <QUrl> 5 + #include <QVector> 6 + 7 + struct Bookmark { 8 + QString title; 9 + QUrl url; 10 + }; 11 + 12 + class BookmarkStore final : public QObject { 13 + Q_OBJECT 14 + public: 15 + explicit BookmarkStore(QObject *parent = nullptr); 16 + 17 + QVector<Bookmark> bookmarks(const QString &profileName) const; 18 + bool contains(const QString &profileName, const QUrl &url) const; 19 + 20 + public slots: 21 + void addBookmark(const QString &profileName, const QString &title, const QUrl &url); 22 + void removeBookmark(const QString &profileName, const QUrl &url); 23 + 24 + signals: 25 + void bookmarksChanged(const QString &profileName); 26 + 27 + private: 28 + QString sanitize(const QString &name) const; 29 + QString filePath(const QString &profileName) const; 30 + QVector<Bookmark> read(const QString &profileName) const; 31 + void writeBookmarks(const QString &profileName, const QVector<Bookmark> &bookmarks) const; 32 + };
+36
src/services/ProfileStore.cpp
··· 3 3 #include "WebKitProfile.hpp" 4 4 5 5 #include <QFileInfo> 6 + #include <QSettings> 6 7 #include <QStandardPaths> 7 8 8 9 ProfileStore::ProfileStore(QObject *parent) : QObject(parent) { ··· 23 24 return m_currentName; 24 25 } 25 26 27 + QString ProfileStore::iconName(const QString &name) const { 28 + const QString clean = sanitize(name); 29 + if (clean.isEmpty()) return QStringLiteral("person.crop.circle.fill"); 30 + QSettings settings(root().filePath(clean + "/profile.ini"), QSettings::IniFormat); 31 + return settings.value("profile/icon", QStringLiteral("person.crop.circle.fill")).toString(); 32 + } 33 + 26 34 WebKitProfile *ProfileStore::currentProfile() const { 27 35 return m_currentProfile; 28 36 } ··· 40 48 const QString clean = sanitize(name); 41 49 if (clean.isEmpty()) return; 42 50 root().mkpath(clean); 51 + emit profilesChanged(); 52 + } 53 + 54 + void ProfileStore::renameProfile(const QString &oldName, const QString &newName) { 55 + const QString oldClean = sanitize(oldName); 56 + const QString newClean = sanitize(newName); 57 + if (oldClean.isEmpty() || newClean.isEmpty() || oldClean == newClean) return; 58 + QDir dir = root(); 59 + if (dir.exists(newClean)) return; 60 + if (!dir.rename(oldClean, newClean)) return; 61 + if (auto *cached = m_cache.take(oldClean)) { 62 + cached->deleteLater(); 63 + } 64 + if (m_currentName == oldClean) { 65 + m_currentName = newClean; 66 + m_currentProfile = loadProfile(newClean); 67 + emit currentProfileChanged(m_currentProfile); 68 + } 69 + emit profilesChanged(); 70 + } 71 + 72 + void ProfileStore::setIconName(const QString &name, const QString &iconName) { 73 + const QString clean = sanitize(name); 74 + const QString icon = iconName.trimmed(); 75 + if (clean.isEmpty() || icon.isEmpty()) return; 76 + root().mkpath(clean); 77 + QSettings settings(root().filePath(clean + "/profile.ini"), QSettings::IniFormat); 78 + settings.setValue("profile/icon", icon); 43 79 emit profilesChanged(); 44 80 } 45 81
+3
src/services/ProfileStore.hpp
··· 14 14 15 15 QStringList profiles() const; 16 16 QString currentName() const; 17 + QString iconName(const QString &name) const; 17 18 WebKitProfile *currentProfile() const; 18 19 19 20 public slots: 20 21 void setCurrentProfile(const QString &name); 21 22 void createProfile(const QString &name); 23 + void renameProfile(const QString &oldName, const QString &newName); 24 + void setIconName(const QString &name, const QString &iconName); 22 25 23 26 signals: 24 27 void currentProfileChanged(WebKitProfile *profile);
+2 -1
src/ui/LayoutMetrics.hpp
··· 14 14 inline constexpr int FloatingPanelRadius = 12; 15 15 inline constexpr int UnifiedToolbarHeight = 52; 16 16 inline constexpr int TrafficLightInteractionInset = 28; 17 - inline constexpr int SidebarMinimumWidth = 190; 17 + inline constexpr int SidebarMinimumWidth = 150; 18 + inline constexpr int SidebarHeaderMinimumWidth = 190; 18 19 inline constexpr int SidebarMaximumWidth = 520; 19 20 inline constexpr int SidebarDefaultWidth = 240; 20 21 inline constexpr int SplitterHandleWidth = 8;
+5 -2
src/ui/SidebarController.cpp
··· 188 188 void SidebarController::positionFloating() { 189 189 if (!m_floating || !m_window) return; 190 190 const int saved = QSettings().value("ui/sidebarWidth", ui::metrics::SidebarDefaultWidth).toInt(); 191 - const int width = qBound(ui::metrics::SidebarMinimumWidth, saved, ui::metrics::SidebarMaximumWidth); 191 + const QWidget *side = m_splitter ? m_splitter->widget(0) : nullptr; 192 + const int minimum = side ? side->minimumWidth() : ui::metrics::SidebarMinimumWidth; 193 + const int width = qBound(minimum, saved, ui::metrics::SidebarMaximumWidth); 192 194 const QRect geometry = ui::metrics::floatingSidebarRect(m_window, width); 193 195 m_floating->setGeometry(geometry); 194 196 if (m_floatingInner) { ··· 304 306 305 307 void SidebarController::dockContent() { 306 308 if (m_content && m_dockedLayout) { 307 - m_dockedLayout->addWidget(m_content, 1); 309 + const int insertAt = qMax(0, m_dockedLayout->count() - 1); 310 + m_dockedLayout->insertWidget(insertAt, m_content, 1); 308 311 } 309 312 } 310 313