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 fd6075a4 c5ecd31d

+544 -72
+152 -18
src/app/BrowserWindow.cpp
··· 23 23 #include <QEvent> 24 24 #include <QFocusEvent> 25 25 #include <QHBoxLayout> 26 + #include <QIcon> 26 27 #include <QJsonArray> 27 28 #include <QJsonDocument> 28 29 #include <QJsonObject> ··· 40 41 #include <QNetworkRequest> 41 42 #include <QProgressBar> 42 43 #include <QPropertyAnimation> 44 + #include <QPixmap> 43 45 #include <QDir> 44 46 #include <QShortcut> 45 47 #include <QShortcutEvent> ··· 180 182 } 181 183 182 184 void BrowserWindow::loadFromOmnibox() { 183 - if (auto *view = currentView()) view->load(urlFromInput(m_omnibox->text())); 185 + const QUrl url = urlFromInput(m_omnibox->text()); 186 + if (handleInternalUrl(url)) return; 187 + if (auto *view = currentView()) view->load(url); 184 188 } 185 189 186 190 void BrowserWindow::showCopiedLinkPopup() { ··· 253 257 mac::refreshUnifiedToolbar(this); 254 258 auto *view = currentView(); 255 259 if (!view) return; 260 + m_tabRecency.removeAll(view); 261 + m_tabRecency.prepend(view); 256 262 static_cast<QStackedLayout *>(m_stack->layout())->setCurrentWidget(view); 257 263 m_omnibox->setText(view->url().toString()); 258 264 if (m_addressBarCtl) { 259 265 m_addressBarCtl->setDisplayUrl(view->url().toString(), view->url().scheme() == "https"); 260 266 } 261 267 setWindowTitle((view->title().isEmpty() ? "pocb" : view->title()) + " — pocb"); 268 + rememberCurrentPage(); 262 269 } 263 270 264 271 QWidget *BrowserWindow::buildTopbar(QWidget *parent) { ··· 310 317 auto reload = [this] { if (auto *v = currentView()) v->reload(); }; 311 318 auto newTab = [this] { 312 319 m_tabTree->newTab(QUrl("about:blank")); 320 + refreshFloatingOmniboxItems(); 313 321 m_floatingOmnibox->showFor(m_stack, QString()); 314 322 }; 315 323 auto settings = [this] { showSettings(); }; ··· 333 341 m_addressBarCtl = new AddressBarController(m_addressBar, m_lockIcon, m_theme, this); 334 342 m_addressBarCtl->setSearchEngineUrl(m_searchEngine); 335 343 connect(m_addressBarCtl, &AddressBarController::submitted, this, [this](const QString &text) { 336 - if (auto *view = currentView()) view->load(urlFromInput(text)); 344 + const QUrl url = urlFromInput(text); 345 + if (!handleInternalUrl(url)) { 346 + if (auto *view = currentView()) view->load(url); 347 + } 337 348 if (auto *v = currentView()) v->setFocus(); 338 349 }); 339 350 connect(m_addressBarCtl, &AddressBarController::escapePressed, this, [this] { ··· 353 364 "<style>html,body{margin:0;height:100%%;background:%1;}</style>" 354 365 "</head><body></body></html>").arg(bg)); 355 366 } 367 + refreshFloatingOmniboxItems(); 356 368 m_floatingOmnibox->showFor(m_stack, QString()); 357 369 }); 358 370 ··· 561 573 QUrl BrowserWindow::urlFromInput(const QString &input) const { 562 574 const QString trimmed = input.trimmed(); 563 575 if (trimmed.isEmpty()) return QUrl(m_homePage); 564 - QUrl url = QUrl::fromUserInput(trimmed); 565 - const bool looksLikeHost = trimmed.contains('.') || trimmed.startsWith("localhost") || trimmed.startsWith("http://") || trimmed.startsWith("https://"); 566 - if (looksLikeHost && url.isValid()) return url; 576 + if (trimmed.startsWith(QStringLiteral("pocb://"), Qt::CaseInsensitive)) return QUrl(trimmed); 577 + const bool hasScheme = trimmed.contains("://"); 578 + const bool isLocalhost = trimmed.startsWith("localhost") || trimmed.startsWith("127.") || trimmed.startsWith("[::1]"); 579 + const bool looksLikeHost = trimmed.contains('.') || isLocalhost || trimmed.startsWith("http://") || trimmed.startsWith("https://"); 580 + if (looksLikeHost) { 581 + const QString navigable = (!hasScheme && !isLocalhost) ? QStringLiteral("https://") + trimmed : trimmed; 582 + QUrl url = QUrl::fromUserInput(navigable); 583 + if (url.isValid()) return url; 584 + } 567 585 return QUrl(m_searchEngine.arg(QString::fromUtf8(QUrl::toPercentEncoding(trimmed)))); 568 586 } 569 587 ··· 571 589 return m_tabTree ? m_tabTree->currentView() : nullptr; 572 590 } 573 591 592 + bool BrowserWindow::handleInternalUrl(const QUrl &url) { 593 + if (url.scheme() != QStringLiteral("pocb")) return false; 594 + const QString command = url.host().toLower(); 595 + if (command == QStringLiteral("settings")) { 596 + showSettings(); 597 + } else if (command == QStringLiteral("close-sidebar")) { 598 + if (m_sidebar) m_sidebar->setHidden(true); 599 + } else if (command == QStringLiteral("toggle-sidebar")) { 600 + if (m_sidebar && m_sidebarWidget) m_sidebar->setHidden(m_sidebarWidget->isVisible()); 601 + } else if (command == QStringLiteral("new-tab")) { 602 + if (m_tabTree) m_tabTree->newTab(QUrl("about:blank")); 603 + } else if (command == QStringLiteral("close-tab")) { 604 + if (m_tabTree) m_tabTree->closeCurrent(); 605 + } else if (command == QStringLiteral("copy-url")) { 606 + if (auto *v = currentView()) { 607 + QApplication::clipboard()->setText(v->url().toString()); 608 + showCopiedLinkPopup(); 609 + } 610 + } else if (command == QStringLiteral("switch-tab")) { 611 + bool ok = false; 612 + const quintptr ptr = url.query().toULongLong(&ok, 16); 613 + if (ok && m_tabTree) m_tabTree->selectView(reinterpret_cast<WebView *>(ptr)); 614 + } 615 + return true; 616 + } 617 + 618 + void BrowserWindow::rememberCurrentPage() { 619 + auto *view = currentView(); 620 + if (!view) return; 621 + const QUrl url = view->url(); 622 + if (!url.isValid() || url.isEmpty() || url.scheme() == QStringLiteral("about") || url.scheme() == QStringLiteral("data")) return; 623 + const QString title = view->title().isEmpty() ? url.toString() : view->title(); 624 + for (int i = m_recentPages.size() - 1; i >= 0; --i) { 625 + if (m_recentPages.at(i).url == url) m_recentPages.removeAt(i); 626 + } 627 + m_recentPages.prepend({title, url}); 628 + while (m_recentPages.size() > 25) m_recentPages.removeLast(); 629 + } 630 + 631 + void BrowserWindow::refreshFloatingOmniboxItems() { 632 + if (!m_floatingOmnibox) return; 633 + QList<FloatingOmnibox::LocalItem> items; 634 + auto addCommand = [this, &items](const QString &title, const QString &url, const QString &symbol) { 635 + items.append({title, url, mac::sfSymbolIcon(symbol, 13.0, m_theme.foreground), false}); 636 + }; 637 + addCommand("Settings", "pocb://settings", "gearshape"); 638 + addCommand("Close Sidebar", "pocb://close-sidebar", "sidebar.left"); 639 + addCommand("Toggle Sidebar", "pocb://toggle-sidebar", "sidebar.left"); 640 + addCommand("New Tab", "pocb://new-tab", "plus"); 641 + addCommand("Close Tab", "pocb://close-tab", "xmark"); 642 + addCommand("Copy Current URL", "pocb://copy-url", "link"); 643 + auto iconForUrl = [this](const QUrl &url) { 644 + if (m_favicons) { 645 + if (const QPixmap pm = m_favicons->cached(url); !pm.isNull()) return QIcon(pm); 646 + m_favicons->request(url); 647 + } 648 + return mac::sfSymbolIcon("globe", 13.0, m_theme.muted); 649 + }; 650 + auto addUrlItem = [&items, &iconForUrl](const QString &title, const QUrl &url) { 651 + if (!url.isValid() || url.isEmpty() || url.scheme() == QStringLiteral("about") || url.scheme() == QStringLiteral("data")) return; 652 + items.append({title.isEmpty() ? url.toString() : title, url.toString(), iconForUrl(url), false}); 653 + }; 654 + QList<WebView *> orderedTabs = m_tabRecency; 655 + if (m_tabTree) { 656 + for (auto *view : m_tabTree->views()) { 657 + if (view && !orderedTabs.contains(view)) orderedTabs.append(view); 658 + } 659 + } 660 + int defaultTabCount = 0; 661 + for (auto *view : orderedTabs) { 662 + if (!view || view == currentView()) continue; 663 + const QUrl url = view->url(); 664 + if (!url.isValid() || url.isEmpty() || url.scheme() == QStringLiteral("about") || url.scheme() == QStringLiteral("data")) continue; 665 + const QString title = view->title().isEmpty() ? url.toString() : view->title(); 666 + items.append({title, QStringLiteral("pocb://switch-tab?") + QString::number(reinterpret_cast<quintptr>(view), 16), iconForUrl(url), defaultTabCount < 3}); 667 + ++defaultTabCount; 668 + } 669 + for (const RecentPage &page : m_recentPages) { 670 + addUrlItem(page.title, page.url); 671 + } 672 + for (const Bookmark &bookmark : m_bookmarks.bookmarks(m_profiles.currentName())) { 673 + addUrlItem(bookmark.title, bookmark.url); 674 + } 675 + m_floatingOmnibox->setLocalItems(items); 676 + } 677 + 574 678 void BrowserWindow::setupUi() { 575 679 qApp->setStyleSheet(appStyleSheet(m_theme)); 576 680 auto *central = new QWidget(this); ··· 589 693 m_floatingOmnibox->setSearchEngineUrl(m_searchEngine); 590 694 connect(m_floatingOmnibox, &FloatingOmnibox::submitted, this, [this](const QString &text) { 591 695 if (text.trimmed().isEmpty()) return; 696 + const QUrl url = urlFromInput(text); 697 + if (handleInternalUrl(url)) return; 592 698 m_omnibox->setText(text); 593 - if (auto *view = currentView()) view->load(urlFromInput(text)); 699 + if (auto *view = currentView()) view->load(url); 594 700 }); 595 701 596 702 // The load progress is now painted inside the address pill (see ··· 816 922 // visual dimensions: ~26 px tall, 6 px horizontal inner padding, 817 923 // same 6 px corner radius. 818 924 if (m_addrWrap) { 925 + if (auto *oldLayout = m_addrWrap->parentWidget() ? m_addrWrap->parentWidget()->layout() : nullptr) { 926 + oldLayout->removeWidget(m_addrWrap); 927 + } 819 928 m_addrWrap->setParent(m_sidebarHeader); 820 - m_addrWrap->setFixedHeight(30); 929 + m_addrWrap->setFixedHeight(36); 821 930 if (auto *pill = qobject_cast<ui::AddrPill *>(m_addrWrap)) { 822 - pill->setRadius(6); 931 + pill->setRadius(8); 823 932 } 824 933 if (auto *row = qobject_cast<QHBoxLayout *>(m_addrWrap->layout())) { 825 - row->setContentsMargins(6, 0, 6, 0); 826 - row->setSpacing(6); 934 + row->setContentsMargins(10, 0, 10, 0); 935 + row->setSpacing(8); 827 936 row->setSizeConstraint(QLayout::SetNoConstraint); 828 937 } 829 - m_addrWrap->setMinimumHeight(30); 830 - m_addrWrap->setMaximumHeight(30); 938 + m_addrWrap->setMinimumHeight(36); 939 + m_addrWrap->setMaximumHeight(36); 831 940 if (m_searchIcon) { 832 - m_searchIcon->setFixedSize(16, 16); 833 - m_searchIcon->setPixmap(mac::sfSymbolIcon("magnifyingglass", 13.0, m_theme.muted).pixmap(16, 16)); 941 + m_searchIcon->setFixedSize(18, 18); 942 + m_searchIcon->setPixmap(mac::sfSymbolIcon("magnifyingglass", 13.5, m_theme.muted).pixmap(18, 18)); 834 943 } 835 - if (m_lockIcon) m_lockIcon->setFixedSize(16, 16); 944 + if (m_lockIcon) m_lockIcon->setFixedSize(18, 18); 945 + if (m_pillMenuBtn) { 946 + m_pillMenuBtn->setFixedSize(24, 24); 947 + m_pillMenuBtn->setIconSize(QSize(16, 16)); 948 + m_pillMenuBtn->setIcon(mac::sfSymbolIcon("ellipsis.circle", 14.0, m_theme.foreground)); 949 + } 836 950 if (m_addressBar) { 837 - m_addressBar->setFixedHeight(22); 951 + m_addressBar->setFixedHeight(28); 952 + m_addressBar->setStyleSheet(QString( 953 + "QLineEdit {" 954 + " background: transparent;" 955 + " border: none;" 956 + " color: %1;" 957 + " font-family: '%2';" 958 + " font-size: 14px;" 959 + " padding: 0px;" 960 + "}" ) 961 + .arg(m_theme.foreground.name(), m_theme.fontFamily)); 838 962 m_addressBar->setContentsMargins(0, 0, 0, 0); 839 963 m_addressBar->setTextMargins(0, 0, 0, 0); 840 964 } 841 - headerCol->addWidget(m_addrWrap); 965 + auto *addrHost = new QWidget(m_sidebarHeader); 966 + addrHost->setObjectName("SidebarAddressHost"); 967 + addrHost->setStyleSheet("QWidget#SidebarAddressHost { background: transparent; }"); 968 + auto *addrHostLayout = new QHBoxLayout(addrHost); 969 + addrHostLayout->setContentsMargins(6, 0, 6, 0); 970 + addrHostLayout->setSpacing(0); 971 + addrHostLayout->addWidget(m_addrWrap); 972 + headerCol->addWidget(addrHost); 842 973 } 843 974 844 975 pageLayout->insertWidget(0, m_sidebarHeader); ··· 874 1005 connect(m_tabTree, &TabTree::currentTabChanged, this, [this] { 875 1006 updateForCurrentTab(); 876 1007 updateCurrentProfileSnapshot(); 1008 + refreshFloatingOmniboxItems(); 877 1009 }); 878 1010 connect(m_tabTree, &TabTree::loadProgress, this, [this](int progress) { 879 1011 if (auto *pill = qobject_cast<ui::AddrPill *>(m_addrWrap)) { ··· 904 1036 "<style>html,body{margin:0;height:100%%;background:%1;}</style>" 905 1037 "</head><body></body></html>").arg(bg)); 906 1038 } 1039 + refreshFloatingOmniboxItems(); 907 1040 m_floatingOmnibox->showFor(m_stack, QString()); 908 1041 }; 909 1042 auto focusOmnibox = [this] { ··· 913 1046 const QString s = u.toString(); 914 1047 if (!s.isEmpty() && s != "about:blank" && !s.startsWith("data:")) current = s; 915 1048 } 1049 + refreshFloatingOmniboxItems(); 916 1050 m_floatingOmnibox->showFor(m_stack, current); 917 1051 }; 918 1052 auto toggleSidebar = [this] { ··· 1218 1352 } 1219 1353 1220 1354 void BrowserWindow::applyChromeForPageColor(const QColor &pageColor) { 1221 - if (!m_topbar) return; 1355 + if (!m_topbar || m_addrInSidebar) return; 1222 1356 1223 1357 const bool hasColor = pageColor.isValid() && pageColor.alpha() >= 16; 1224 1358 const QColor bg = hasColor ? pageColor : QColor(28, 28, 30, 235);
+9
src/app/BrowserWindow.hpp
··· 75 75 void updateSidebarPreview(int direction); 76 76 void showProfileMenu(); 77 77 void showCopiedLinkPopup(); 78 + void refreshFloatingOmniboxItems(); 79 + void rememberCurrentPage(); 80 + bool handleInternalUrl(const QUrl &url); 81 + struct RecentPage { 82 + QString title; 83 + QUrl url; 84 + }; 78 85 Theme m_theme; 79 86 ProfileStore m_profiles; 80 87 BookmarkStore m_bookmarks; ··· 129 136 FaviconService *m_favicons = nullptr; 130 137 QHBoxLayout *m_toolbarLayout = nullptr; 131 138 SidebarController *m_sidebar = nullptr; 139 + QList<WebView *> m_tabRecency; 140 + QList<RecentPage> m_recentPages; 132 141 };
+224 -13
src/mac/NativeSettingsWindow.mm
··· 1 1 #include "NativeSettingsWindow.hpp" 2 2 3 + #include "ChromeExtensionManager.hpp" 3 4 #include "ProfileStore.hpp" 4 5 6 + #include <QSettings> 7 + #include <QStringList> 5 8 #include <QWidget> 6 9 7 10 #import <AppKit/AppKit.h> 8 11 9 12 namespace { 10 13 14 + NSString *toNSString(const QString &value) { 15 + return [NSString stringWithUTF8String:value.toUtf8().constData()]; 16 + } 17 + 18 + QString toQString(NSString *value) { 19 + if (!value) return QString(); 20 + return QString::fromUtf8(value.UTF8String); 21 + } 22 + 11 23 NSWindow *parentWindow(QWidget *parent) { 12 24 if (!parent) return NSApp.keyWindow; 13 25 NSView *view = (__bridge NSView *)(reinterpret_cast<void *>(parent->winId())); 14 26 return view.window ?: NSApp.keyWindow; 15 27 } 16 28 29 + NSTextField *label(NSString *text) { 30 + NSTextField *field = [NSTextField labelWithString:text]; 31 + field.font = [NSFont systemFontOfSize:12.0 weight:NSFontWeightMedium]; 32 + field.textColor = NSColor.secondaryLabelColor; 33 + field.alignment = NSTextAlignmentRight; 34 + field.translatesAutoresizingMaskIntoConstraints = NO; 35 + return field; 36 + } 37 + 38 + NSTextField *textField(NSString *value, NSString *placeholder) { 39 + NSTextField *field = [NSTextField textFieldWithString:value ?: @""]; 40 + field.placeholderString = placeholder; 41 + field.font = [NSFont systemFontOfSize:13.0]; 42 + field.translatesAutoresizingMaskIntoConstraints = NO; 43 + return field; 44 + } 45 + 46 + NSButton *button(NSString *title, NSBezelStyle style) { 47 + NSButton *control = [NSButton buttonWithTitle:title target:nil action:nil]; 48 + control.bezelStyle = style; 49 + control.translatesAutoresizingMaskIntoConstraints = NO; 50 + return control; 51 + } 52 + 53 + NSButton *checkbox(NSString *title, bool checked) { 54 + NSButton *control = [NSButton checkboxWithTitle:title target:nil action:nil]; 55 + control.state = checked ? NSControlStateValueOn : NSControlStateValueOff; 56 + control.translatesAutoresizingMaskIntoConstraints = NO; 57 + return control; 58 + } 59 + 60 + NSView *separator() { 61 + NSBox *box = [[NSBox alloc] init]; 62 + box.boxType = NSBoxSeparator; 63 + box.translatesAutoresizingMaskIntoConstraints = NO; 64 + return box; 65 + } 66 + 67 + void addRow(NSGridView *grid, NSString *title, NSView *control) { 68 + NSGridRow *row = [grid addRowWithViews:@[label(title), control]]; 69 + row.yPlacement = NSGridCellPlacementCenter; 70 + } 71 + 17 72 void positionWindow(NSWindow *window, NSWindow *owner) { 18 73 if (!owner) { 19 74 [window center]; ··· 35 90 QString &homePage, 36 91 QString &searchEngine, 37 92 bool &showFullUrl) { 38 - Q_UNUSED(profiles); 39 - Q_UNUSED(homePage); 40 - Q_UNUSED(searchEngine); 41 - Q_UNUSED(showFullUrl); 93 + @autoreleasepool { 94 + QSettings settings; 95 + __block NSInteger result = NSModalResponseCancel; 42 96 43 - @autoreleasepool { 44 97 NSWindow *window = [[NSWindow alloc] 45 - initWithContentRect:NSMakeRect(0, 0, 640, 520) 98 + initWithContentRect:NSMakeRect(0, 0, 680, 520) 46 99 styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView 47 100 backing:NSBackingStoreBuffered 48 101 defer:NO]; ··· 50 103 window.titlebarAppearsTransparent = YES; 51 104 window.movableByWindowBackground = YES; 52 105 window.releasedWhenClosed = NO; 53 - window.minSize = NSMakeSize(480, 320); 106 + window.minSize = NSMakeSize(560, 440); 54 107 55 108 NSVisualEffectView *root = [[NSVisualEffectView alloc] init]; 56 109 root.material = NSVisualEffectMaterialUnderWindowBackground; 57 110 root.blendingMode = NSVisualEffectBlendingModeBehindWindow; 58 111 root.state = NSVisualEffectStateActive; 112 + root.translatesAutoresizingMaskIntoConstraints = NO; 59 113 window.contentView = root; 60 114 61 - __block bool closed = false; 62 - id observer = [[NSNotificationCenter defaultCenter] 115 + NSStackView *stack = [[NSStackView alloc] init]; 116 + stack.orientation = NSUserInterfaceLayoutOrientationVertical; 117 + stack.alignment = NSLayoutAttributeLeading; 118 + stack.spacing = 18.0; 119 + stack.translatesAutoresizingMaskIntoConstraints = NO; 120 + [root addSubview:stack]; 121 + 122 + NSTextField *title = [NSTextField labelWithString:@"Settings"]; 123 + title.font = [NSFont systemFontOfSize:26.0 weight:NSFontWeightSemibold]; 124 + title.textColor = NSColor.labelColor; 125 + title.translatesAutoresizingMaskIntoConstraints = NO; 126 + [stack addArrangedSubview:title]; 127 + 128 + NSGridView *grid = [[NSGridView alloc] init]; 129 + grid.translatesAutoresizingMaskIntoConstraints = NO; 130 + grid.rowSpacing = 12.0; 131 + grid.columnSpacing = 14.0; 132 + [stack addArrangedSubview:grid]; 133 + 134 + NSPopUpButton *profilePopup = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; 135 + profilePopup.translatesAutoresizingMaskIntoConstraints = NO; 136 + for (const QString &name : profiles.profiles()) [profilePopup addItemWithTitle:toNSString(name)]; 137 + [profilePopup selectItemWithTitle:toNSString(profiles.currentName())]; 138 + addRow(grid, @"Active profile", profilePopup); 139 + 140 + NSStackView *newProfileRow = [[NSStackView alloc] init]; 141 + newProfileRow.orientation = NSUserInterfaceLayoutOrientationHorizontal; 142 + newProfileRow.spacing = 8.0; 143 + newProfileRow.translatesAutoresizingMaskIntoConstraints = NO; 144 + NSTextField *newProfile = textField(@"", @"New profile name"); 145 + NSButton *createProfile = button(@"Create", NSBezelStyleRounded); 146 + [newProfileRow addArrangedSubview:newProfile]; 147 + [newProfileRow addArrangedSubview:createProfile]; 148 + [newProfile.widthAnchor constraintGreaterThanOrEqualToConstant:260.0].active = YES; 149 + addRow(grid, @"Add profile", newProfileRow); 150 + 151 + [grid addRowWithViews:@[label(@""), separator()]]; 152 + 153 + NSTextField *homeField = textField(toNSString(homePage), @"https://search.brave.com"); 154 + addRow(grid, @"Home page", homeField); 155 + 156 + NSTextField *searchField = textField(toNSString(searchEngine), @"https://search.brave.com/search?q=%1"); 157 + addRow(grid, @"Search URL", searchField); 158 + 159 + NSButton *fullUrl = checkbox(@"Always show the full URL", showFullUrl); 160 + addRow(grid, @"Address bar", fullUrl); 161 + 162 + NSButton *sidebarAddress = checkbox(@"Move address bar into the sidebar (restart required)", settings.value("ui/addressBarInSidebar", false).toBool()); 163 + addRow(grid, @"", sidebarAddress); 164 + 165 + [grid addRowWithViews:@[label(@""), separator()]]; 166 + 167 + NSStackView *extensionRow = [[NSStackView alloc] init]; 168 + extensionRow.orientation = NSUserInterfaceLayoutOrientationHorizontal; 169 + extensionRow.spacing = 8.0; 170 + extensionRow.translatesAutoresizingMaskIntoConstraints = NO; 171 + NSTextField *extensionPaths = textField(toNSString(ChromeExtensionManager::configuredPaths().join(";")), @"/path/to/unpacked-extension;/path/to/another-extension"); 172 + NSButton *chooseExtension = button(@"Add Folder", NSBezelStyleRounded); 173 + [extensionRow addArrangedSubview:extensionPaths]; 174 + [extensionRow addArrangedSubview:chooseExtension]; 175 + [extensionPaths.widthAnchor constraintGreaterThanOrEqualToConstant:360.0].active = YES; 176 + addRow(grid, @"Extensions", extensionRow); 177 + 178 + NSTextField *help = [NSTextField wrappingLabelWithString:@"Search URLs must contain %1. Extension folders are loaded as local WebExtensions when supported by WebKit."]; 179 + help.font = [NSFont systemFontOfSize:12.0]; 180 + help.textColor = NSColor.secondaryLabelColor; 181 + help.translatesAutoresizingMaskIntoConstraints = NO; 182 + addRow(grid, @"", help); 183 + 184 + [grid columnAtIndex:0].width = 116.0; 185 + [grid columnAtIndex:0].xPlacement = NSGridCellPlacementTrailing; 186 + [grid columnAtIndex:1].xPlacement = NSGridCellPlacementFill; 187 + 188 + NSStackView *footer = [[NSStackView alloc] init]; 189 + footer.orientation = NSUserInterfaceLayoutOrientationHorizontal; 190 + footer.alignment = NSLayoutAttributeCenterY; 191 + footer.spacing = 8.0; 192 + footer.translatesAutoresizingMaskIntoConstraints = NO; 193 + NSView *spacer = [[NSView alloc] init]; 194 + spacer.translatesAutoresizingMaskIntoConstraints = NO; 195 + NSButton *cancel = button(@"Cancel", NSBezelStyleRounded); 196 + NSButton *save = button(@"Save", NSBezelStyleRounded); 197 + save.keyEquivalent = @"\r"; 198 + save.bezelColor = NSColor.controlAccentColor; 199 + [footer addArrangedSubview:spacer]; 200 + [footer addArrangedSubview:cancel]; 201 + [footer addArrangedSubview:save]; 202 + [stack addArrangedSubview:footer]; 203 + 204 + [NSLayoutConstraint activateConstraints:@[ 205 + [stack.topAnchor constraintEqualToAnchor:root.topAnchor constant:54.0], 206 + [stack.leadingAnchor constraintEqualToAnchor:root.leadingAnchor constant:32.0], 207 + [stack.trailingAnchor constraintEqualToAnchor:root.trailingAnchor constant:-32.0], 208 + [stack.bottomAnchor constraintLessThanOrEqualToAnchor:root.bottomAnchor constant:-24.0], 209 + [grid.widthAnchor constraintEqualToAnchor:stack.widthAnchor], 210 + [footer.widthAnchor constraintEqualToAnchor:stack.widthAnchor], 211 + [spacer.widthAnchor constraintGreaterThanOrEqualToConstant:1.0] 212 + ]]; 213 + 214 + __block id createMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:^NSEvent *(NSEvent *event) { 215 + if (event.window == window && NSPointInRect([createProfile convertPoint:event.locationInWindow fromView:nil], createProfile.bounds)) { 216 + const QString name = toQString(newProfile.stringValue).trimmed(); 217 + if (!name.isEmpty()) { 218 + profiles.createProfile(name); 219 + profiles.setCurrentProfile(name); 220 + [profilePopup removeAllItems]; 221 + for (const QString &profileName : profiles.profiles()) [profilePopup addItemWithTitle:toNSString(profileName)]; 222 + [profilePopup selectItemWithTitle:toNSString(name)]; 223 + newProfile.stringValue = @""; 224 + } 225 + } 226 + if (event.window == window && NSPointInRect([chooseExtension convertPoint:event.locationInWindow fromView:nil], chooseExtension.bounds)) { 227 + NSOpenPanel *panel = [NSOpenPanel openPanel]; 228 + panel.canChooseFiles = NO; 229 + panel.canChooseDirectories = YES; 230 + panel.allowsMultipleSelection = NO; 231 + if ([panel runModal] == NSModalResponseOK) { 232 + QStringList paths = toQString(extensionPaths.stringValue).split(';', Qt::SkipEmptyParts); 233 + const QString path = toQString(panel.URL.path); 234 + if (!paths.contains(path)) paths << path; 235 + extensionPaths.stringValue = toNSString(paths.join(';')); 236 + } 237 + } 238 + return event; 239 + }]; 240 + 241 + __block id keyMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown | NSEventMaskKeyDown handler:^NSEvent *(NSEvent *event) { 242 + if (event.window != window) return event; 243 + const bool cancelClick = event.type == NSEventTypeLeftMouseDown && NSPointInRect([cancel convertPoint:event.locationInWindow fromView:nil], cancel.bounds); 244 + const bool saveClick = event.type == NSEventTypeLeftMouseDown && NSPointInRect([save convertPoint:event.locationInWindow fromView:nil], save.bounds); 245 + const bool escapeKey = event.type == NSEventTypeKeyDown && event.keyCode == 53; 246 + const bool returnKey = event.type == NSEventTypeKeyDown && (event.keyCode == 36 || event.keyCode == 76); 247 + if (cancelClick || escapeKey) { 248 + result = NSModalResponseCancel; 249 + [NSApp stopModal]; 250 + return nil; 251 + } 252 + if (saveClick || returnKey) { 253 + result = NSModalResponseOK; 254 + [NSApp stopModal]; 255 + return nil; 256 + } 257 + return event; 258 + }]; 259 + 260 + id closeObserver = [[NSNotificationCenter defaultCenter] 63 261 addObserverForName:NSWindowWillCloseNotification 64 262 object:window 65 263 queue:nil 66 264 usingBlock:^(NSNotification *) { 67 - closed = true; 265 + result = NSModalResponseCancel; 68 266 [NSApp stopModal]; 69 267 }]; 70 268 ··· 72 270 [window makeKeyAndOrderFront:nil]; 73 271 [NSApp runModalForWindow:window]; 74 272 75 - [[NSNotificationCenter defaultCenter] removeObserver:observer]; 76 - if (!closed) [window close]; 77 - return false; 273 + [[NSNotificationCenter defaultCenter] removeObserver:closeObserver]; 274 + [NSEvent removeMonitor:createMonitor]; 275 + [NSEvent removeMonitor:keyMonitor]; 276 + [window orderOut:nil]; 277 + 278 + if (result != NSModalResponseOK) return false; 279 + 280 + const QString selectedProfile = toQString(profilePopup.titleOfSelectedItem).trimmed(); 281 + if (!selectedProfile.isEmpty()) profiles.setCurrentProfile(selectedProfile); 282 + homePage = toQString(homeField.stringValue).trimmed(); 283 + searchEngine = toQString(searchField.stringValue).trimmed(); 284 + showFullUrl = fullUrl.state == NSControlStateValueOn; 285 + settings.setValue("ui/showFullUrl", showFullUrl); 286 + settings.setValue("ui/addressBarInSidebar", sidebarAddress.state == NSControlStateValueOn); 287 + ChromeExtensionManager::setConfiguredPaths(toQString(extensionPaths.stringValue).split(';', Qt::SkipEmptyParts)); 288 + return true; 78 289 } 79 290 } 80 291
+20 -7
src/tabs/TabTree.cpp
··· 23 23 namespace { 24 24 25 25 QRect closeButtonRect(const QRect &rowRect, int viewportWidth) { 26 - const int side = 18; 26 + const int side = 16; 27 27 const int right = viewportWidth > 0 ? viewportWidth - 6 : rowRect.right() - 6; 28 28 return QRect(right - side, 29 29 rowRect.top() + (rowRect.height() - side) / 2, ··· 40 40 item = item->parent(); 41 41 } 42 42 return depth; 43 + } 44 + 45 + bool isNewTabUrl(const QUrl &url) { 46 + return url.isEmpty() || url.toString() == QStringLiteral("about:blank"); 43 47 } 44 48 45 49 class TabItemDelegate final : public QStyledItemDelegate { ··· 63 67 fill.setAlpha(selected ? 22 : 12); 64 68 QRect rowRect = option.rect; 65 69 rowRect.setLeft(6 + depth * 18); 66 - if (viewportWidth > 0) rowRect.setRight(viewportWidth - 4); 70 + if (viewportWidth > 0) rowRect.setRight(viewportWidth - 6); 67 71 painter->setPen(Qt::NoPen); 68 72 painter->setBrush(fill); 69 - painter->drawRoundedRect(rowRect.adjusted(0, 2, 0, -2), 7, 7); 73 + painter->drawRoundedRect(rowRect.adjusted(0, 3, 0, -3), 6, 6); 70 74 } 71 75 72 76 const QVariant decoration = index.data(Qt::DecorationRole); 73 - QRect textRect = option.rect.adjusted(14 + depth * 18, 0, -30, 0); 77 + QRect textRect = option.rect.adjusted(12 + depth * 18, 0, -28, 0); 74 78 if (decoration.canConvert<QIcon>()) { 75 79 const QIcon icon = qvariant_cast<QIcon>(decoration); 76 - const QRect iconRect(textRect.left(), option.rect.top() + (option.rect.height() - 16) / 2, 16, 16); 80 + const QRect iconRect(textRect.left(), option.rect.top() + (option.rect.height() - 14) / 2, 14, 14); 77 81 icon.paint(painter, iconRect, Qt::AlignCenter); 78 82 textRect.setLeft(iconRect.right() + 7); 79 83 } ··· 128 132 m_tabs->setFocusPolicy(Qt::NoFocus); 129 133 m_tabs->setAnimated(true); 130 134 m_tabs->setFrameShape(QFrame::NoFrame); 131 - m_tabs->setIconSize(QSize(16, 16)); 135 + m_tabs->setIconSize(QSize(14, 14)); 132 136 m_tabs->setExpandsOnDoubleClick(false); 133 137 m_tabs->setUniformRowHeights(true); 134 138 m_tabs->setMouseTracking(true); ··· 144 148 m_tabs->viewport()->setAutoFillBackground(false); 145 149 m_tabs->setStyleSheet(QString( 146 150 "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; }" 151 + "QTreeWidget#TabTree::item { padding: 2px 26px 2px 6px; border: none; background: transparent; color: %1; selection-background-color: transparent; }" 148 152 "QTreeWidget#TabTree::item:selected { background: transparent; color: %1; selection-background-color: transparent; }" 149 153 "QTreeWidget#TabTree::item:selected:active { background: transparent; color: %1; selection-background-color: transparent; }" 150 154 "QTreeWidget#TabTree::item:selected:!active { background: transparent; color: %1; selection-background-color: transparent; }" ··· 160 164 // snappy chrome update, then kick off a fresh sniff in case the 161 165 // page has changed since (scrolled, dynamically restyled, etc.). 162 166 if (auto *v = currentView()) { 167 + if (isNewTabUrl(v->url())) { 168 + emit loadProgress(0); 169 + emit themeColorChanged(QColor()); 170 + return; 171 + } 163 172 const QColor cached = v->cachedThemeColor(); 164 173 if (cached.isValid()) emit themeColorChanged(cached); 165 174 v->sniffTopColor(); ··· 317 326 }); 318 327 connect(view, &WebView::loadProgress, this, [this, view](int progress) { 319 328 if (view != currentView()) return; 329 + if (isNewTabUrl(view->url())) { 330 + emit loadProgress(0); 331 + return; 332 + } 320 333 emit loadProgress(progress); 321 334 }); 322 335 connect(view, &WebView::themeColorChanged, this, [this, view](const QColor &c) {
+3 -6
src/ui/AddressBarController.cpp
··· 215 215 if (ev->type() == QEvent::FocusIn) { 216 216 beginEditing(); 217 217 } else if (ev->type() == QEvent::FocusOut) { 218 - QTimer::singleShot(0, this, [this] { 219 - QWidget *now = QApplication::focusWidget(); 220 - if (now == m_bar || now == m_popup || (m_popupList && now == m_popupList->viewport())) return; 221 - endEditing(/*restoreUrl=*/true, m_savedUrl); 222 - }); 218 + return QObject::eventFilter(obj, ev); 223 219 } else if (ev->type() == QEvent::KeyPress) { 224 220 auto *ke = static_cast<QKeyEvent *>(ev); 225 221 if (ke->key() == Qt::Key_Escape) { ··· 278 274 } 279 275 hidePopup(); 280 276 if (text.trimmed().isEmpty()) return; 281 - m_editing = false; 277 + endEditing(/*restoreUrl=*/false, QString()); 282 278 emit submitted(text); 283 279 } 284 280 ··· 576 572 m_popupList->viewport()->raise(); 577 573 } 578 574 m_popup->raise(); 575 + if (m_bar && !m_bar->hasFocus()) m_bar->setFocus(Qt::OtherFocusReason); 579 576 } 580 577 581 578 void AddressBarController::hidePopup() {
+57 -8
src/ui/ChromeWidgets.cpp
··· 2 2 3 3 #include <QEasingCurve> 4 4 #include <QEnterEvent> 5 + #include <QLinearGradient> 5 6 #include <QPainter> 6 7 #include <QPainterPath> 7 8 #include <QVariantAnimation> ··· 78 79 update(); 79 80 }); 80 81 m_loadAnim = new QVariantAnimation(this); 81 - m_loadAnim->setDuration(180); 82 + m_loadAnim->setDuration(130); 82 83 m_loadAnim->setEasingCurve(QEasingCurve::OutCubic); 83 84 connect(m_loadAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant &v) { 84 85 m_loadCurrent = v.toDouble(); 85 86 update(); 86 87 }); 88 + connect(m_loadAnim, &QVariantAnimation::finished, this, [this] { 89 + if (m_loadTarget >= 100) { 90 + m_loadCurrent = 0.0; 91 + m_loadPulseAnim->stop(); 92 + update(); 93 + } 94 + }); 95 + m_loadPulseAnim = new QVariantAnimation(this); 96 + m_loadPulseAnim->setStartValue(0.0); 97 + m_loadPulseAnim->setEndValue(1.0); 98 + m_loadPulseAnim->setDuration(1100); 99 + m_loadPulseAnim->setLoopCount(-1); 100 + m_loadPulseAnim->setEasingCurve(QEasingCurve::Linear); 101 + connect(m_loadPulseAnim, &QVariantAnimation::valueChanged, this, [this](const QVariant &v) { 102 + m_loadPulse = v.toDouble(); 103 + update(); 104 + }); 87 105 } 88 106 89 107 void AddrPill::setLoadProgress(int percent) { 90 108 if (percent < 0) percent = 0; 91 109 if (percent > 100) percent = 100; 92 110 if (percent == m_loadTarget) return; 111 + if (percent > 0 && percent < 100 && m_loadCurrent >= 100.0) { 112 + m_loadCurrent = 0.0; 113 + } 93 114 m_loadTarget = percent; 94 115 m_loadAnim->stop(); 95 116 m_loadAnim->setStartValue(m_loadCurrent); 96 - // 100% snaps shut quickly: slide to full, then hide via a follow-up to 0. 97 - if (percent <= 0 || percent >= 100) { 98 - m_loadAnim->setEndValue(percent <= 0 ? 0.0 : 100.0); 99 - m_loadAnim->setDuration(percent >= 100 ? 120 : 0); 117 + if (percent <= 0) { 118 + m_loadCurrent = 0.0; 119 + m_loadPulseAnim->stop(); 120 + update(); 121 + return; 122 + } 123 + if (percent >= 100) { 124 + m_loadAnim->setEndValue(100.0); 125 + m_loadAnim->setDuration(115); 100 126 } else { 101 - m_loadAnim->setEndValue((qreal)percent); 102 - m_loadAnim->setDuration(180); 127 + if (m_loadPulseAnim->state() != QAbstractAnimation::Running) m_loadPulseAnim->start(); 128 + const qreal visualTarget = qMax((qreal)percent, 96.0); 129 + m_loadAnim->setEndValue(visualTarget); 130 + const int delta = qAbs(qRound(visualTarget - m_loadCurrent)); 131 + m_loadAnim->setDuration(qBound(70, 28 + delta * 3, 145)); 103 132 } 104 133 m_loadAnim->start(); 105 134 } ··· 174 203 const qreal h = 2.0; 175 204 const qreal w = width() * (m_loadCurrent / 100.0); 176 205 const QRectF strip(0, height() - h, w, h); 177 - p.fillRect(strip, m_loadColor); 206 + QColor load = m_loadColor; 207 + load.setAlphaF(qMin(1.0, load.alphaF() * 0.86)); 208 + p.fillRect(strip, load); 209 + const qreal pulseWidth = qMax<qreal>(26.0, qMin<qreal>(72.0, width() * 0.18)); 210 + const qreal edgePulse = 0.5 - 0.5 * qCos(m_loadPulse * 6.283185307179586); 211 + const qreal center = w - pulseWidth * (0.38 + edgePulse * 0.18); 212 + const qreal left = qMax<qreal>(0.0, center - pulseWidth * 0.62); 213 + const qreal right = qMin<qreal>(w, center + pulseWidth * 0.38); 214 + if (right > left) { 215 + QLinearGradient shine(left, 0.0, right, 0.0); 216 + QColor edge = m_loadColor.lighter(110); 217 + edge.setAlphaF(0.0); 218 + QColor mid = m_loadColor.lighter(155); 219 + mid.setAlphaF(qMin(1.0, mid.alphaF() * (0.52 + edgePulse * 0.36))); 220 + QColor tip = m_loadColor.lighter(170); 221 + tip.setAlphaF(qMin(1.0, tip.alphaF() * (0.70 + edgePulse * 0.25))); 222 + shine.setColorAt(0.0, edge); 223 + shine.setColorAt(0.58, mid); 224 + shine.setColorAt(1.0, tip); 225 + p.fillRect(QRectF(left, height() - h, right - left, h), shine); 226 + } 178 227 p.restore(); 179 228 } 180 229 }
+2
src/ui/ChromeWidgets.hpp
··· 65 65 66 66 int m_loadTarget = 0; 67 67 qreal m_loadCurrent = 0.0; 68 + qreal m_loadPulse = 0.0; 68 69 QColor m_loadColor = QColor(120, 180, 255, 235); 69 70 QVariantAnimation *m_loadAnim = nullptr; 71 + QVariantAnimation *m_loadPulseAnim = nullptr; 70 72 }; 71 73 72 74 } // namespace ui
+61 -19
src/ui/FloatingOmnibox.cpp
··· 24 24 // Vicinae LauncherWindow.qml: 60px search row, 14px corner rounding, 25 25 // 1px mainWindowBorder, no shadow, full-window translucent fill. 26 26 constexpr int kPanelWidth = 720; 27 + constexpr int kPanelSideInset = 24; 27 28 constexpr int kInputHeight = 60; 28 - constexpr int kRowHeight = 40; 29 - constexpr int kMaxRows = 7; 29 + constexpr int kRowHeight = 46; 30 + constexpr int kMaxRows = 5; 30 31 constexpr int kPanelRadius = 14; 31 32 constexpr int kInputPadX = 16; // SearchBar.qml leftMargin/rightMargin 32 33 constexpr int kListPadV = 4; // GenericListView topMargin/bottomMargin ··· 187 188 connect(m_input, &QLineEdit::textEdited, this, &FloatingOmnibox::onTextEdited); 188 189 connect(m_input, &QLineEdit::returnPressed, this, &FloatingOmnibox::acceptCurrent); 189 190 connect(m_list, &QListWidget::itemActivated, this, [this](QListWidgetItem *item) { 190 - if (item) emit submitted(item->text()); 191 + if (item) emit submitted(item->data(Qt::UserRole).toString()); 191 192 close(); 192 193 }); 193 194 connect(m_list, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) { 194 - if (item) emit submitted(item->text()); 195 + if (item) emit submitted(item->data(Qt::UserRole).toString()); 195 196 close(); 196 197 }); 197 198 ··· 205 206 } 206 207 207 208 void FloatingOmnibox::showFor(QWidget *anchor, const QString &initialText) { 209 + m_anchorWidth = anchor ? anchor->width() : 0; 210 + m_searchSuggestions.clear(); 211 + rebuildSuggestions(); 212 + m_input->setText(initialText); 213 + m_input->selectAll(); 214 + relayout(); 208 215 if (anchor) { 209 216 const QPoint topLeft = anchor->mapToGlobal(QPoint(0, 0)); 210 - const int x = topLeft.x() + (anchor->width() - width()) / 2; 217 + const int x = topLeft.x() + qBound(kPanelSideInset, (anchor->width() - width()) / 2, qMax(kPanelSideInset, anchor->width() - width() - kPanelSideInset)); 211 218 // Vicinae sits at Screen.height/3; over the web viewport, ~38% reads similarly. 212 219 const int y = topLeft.y() + (anchor->height() - height()) * 38 / 100; 213 220 move(x, y); 214 221 } 215 - setSuggestions({}); 216 - m_input->setText(initialText); 217 - m_input->selectAll(); 218 222 show(); 219 223 raise(); 220 224 activateWindow(); ··· 276 280 277 281 void FloatingOmnibox::onTextEdited(const QString &text) { 278 282 m_pendingQuery = text.trimmed(); 279 - if (m_pendingQuery.isEmpty()) { setSuggestions({}); return; } 283 + if (m_pendingQuery.isEmpty()) { 284 + m_searchSuggestions.clear(); 285 + rebuildSuggestions(); 286 + return; 287 + } 288 + rebuildSuggestions(); 280 289 m_debounce->start(); 281 290 } 282 291 ··· 333 342 } 334 343 } 335 344 if (items.size() > kMaxRows) items = items.mid(0, kMaxRows); 336 - setSuggestions(items); 345 + setSearchSuggestions(items); 346 + } 347 + 348 + void FloatingOmnibox::setLocalItems(const QList<LocalItem> &items) { 349 + m_localItems = items; 350 + rebuildSuggestions(); 351 + } 352 + 353 + void FloatingOmnibox::setSearchSuggestions(const QStringList &items) { 354 + m_searchSuggestions = items; 355 + rebuildSuggestions(); 337 356 } 338 357 339 - void FloatingOmnibox::setSuggestions(const QStringList &items) { 358 + void FloatingOmnibox::addItem(const QString &title, const QString &value, const QIcon &icon) { 359 + auto *it = new QListWidgetItem(icon, title, m_list); 360 + it->setData(Qt::UserRole, value); 361 + it->setSizeHint(QSize(0, kRowHeight)); 362 + } 363 + 364 + void FloatingOmnibox::rebuildSuggestions() { 340 365 m_list->clear(); 341 - if (items.isEmpty()) { 366 + const QString query = m_input ? m_input->text().trimmed() : QString(); 367 + for (const auto &item : m_localItems) { 368 + const bool isTabSwitch = item.value.startsWith(QStringLiteral("pocb://switch-tab?")); 369 + if (query.isEmpty()) { 370 + if (!item.alwaysShow) continue; 371 + } else if (isTabSwitch) { 372 + if (QString::compare(item.title, query, Qt::CaseInsensitive) != 0 && QString::compare(item.value, query, Qt::CaseInsensitive) != 0) continue; 373 + } else if (item.value.startsWith(QStringLiteral("pocb://")) && query.size() < 3) { 374 + continue; 375 + } else if (!item.title.contains(query, Qt::CaseInsensitive) && !item.value.contains(query, Qt::CaseInsensitive)) { 376 + continue; 377 + } 378 + addItem(item.title, item.value, item.icon); 379 + if (m_list->count() >= kMaxRows) break; 380 + } 381 + for (const auto &s : m_searchSuggestions) { 382 + if (m_list->count() >= kMaxRows) break; 383 + if (s.isEmpty()) continue; 384 + addItem(s, s); 385 + } 386 + if (m_list->count() == 0) { 342 387 m_list->hide(); 343 388 m_divider->hide(); 344 389 relayout(); 345 390 return; 346 391 } 347 - for (const auto &s : items) { 348 - auto *it = new QListWidgetItem(s, m_list); 349 - it->setSizeHint(QSize(0, kRowHeight)); 350 - } 351 392 m_divider->show(); 352 393 m_list->show(); 353 - m_list->setCurrentRow(-1); 394 + m_list->setCurrentRow(0); 354 395 relayout(); 355 396 } 356 397 ··· 358 399 int visibleRows = qMin(m_list->count(), kMaxRows); 359 400 int listHeight = visibleRows == 0 ? 0 : visibleRows * kRowHeight + kListPadV * 2; 360 401 int divH = visibleRows == 0 ? 0 : 1; 361 - setFixedSize(kPanelWidth, kInputHeight + divH + listHeight); 402 + const int maxWidth = m_anchorWidth > 0 ? qMax(320, m_anchorWidth - kPanelSideInset * 2) : kPanelWidth; 403 + setFixedSize(qMin(kPanelWidth, maxWidth), kInputHeight + divH + listHeight); 362 404 m_list->setFixedHeight(listHeight); 363 405 } 364 406 365 407 void FloatingOmnibox::acceptCurrent() { 366 408 QString text = m_list->currentItem() 367 - ? m_list->currentItem()->text() 409 + ? m_list->currentItem()->data(Qt::UserRole).toString() 368 410 : m_input->text(); 369 411 emit submitted(text); 370 412 close();
+16 -1
src/ui/FloatingOmnibox.hpp
··· 2 2 3 3 #include "Theme.hpp" 4 4 5 + #include <QIcon> 6 + #include <QList> 5 7 #include <QPointer> 6 8 #include <QWidget> 7 9 ··· 26 28 // the omnibox will derive the engine's public suggest endpoint. 27 29 void setSearchEngineUrl(const QString &templateUrl); 28 30 31 + struct LocalItem { 32 + QString title; 33 + QString value; 34 + QIcon icon; 35 + bool alwaysShow = false; 36 + }; 37 + void setLocalItems(const QList<LocalItem> &items); 38 + 29 39 signals: 30 40 void submitted(const QString &text); 31 41 ··· 40 50 void scheduleSuggestionRequest(); 41 51 void fetchSuggestions(); 42 52 void onSuggestionsReceived(QNetworkReply *reply); 43 - void setSuggestions(const QStringList &items); 53 + void rebuildSuggestions(); 54 + void setSearchSuggestions(const QStringList &items); 55 + void addItem(const QString &title, const QString &value, const QIcon &icon = QIcon()); 44 56 void acceptCurrent(); 45 57 void relayout(); 46 58 ··· 53 65 QTimer *m_debounce = nullptr; 54 66 QString m_pendingQuery; 55 67 QString m_engineHost; // e.g. "duckduckgo.com" 68 + QList<LocalItem> m_localItems; 69 + QStringList m_searchSuggestions; 70 + int m_anchorWidth = 0; 56 71 };