11#include <QLoggingCategory>
13#include "AddressCompleter.h"
14#include "AutoCompleteLineEdit.h"
15#include "HistoryDatabase.h"
17static Q_LOGGING_CATEGORY(log,
"WebBrowser.Address")
28 QHBoxLayout *layout =
new QHBoxLayout(
this);
29 layout->setContentsMargins(8, 4, 8, 4);
30 layout->setSpacing(8);
33 m_iconLabel =
new QLabel(
this);
34 m_iconLabel->setFixedSize(16, 16);
35 QPixmap pixmap = icon.pixmap(16, 16);
36 m_iconLabel->setPixmap(pixmap);
37 layout->addWidget(m_iconLabel);
40 m_titleLabel =
new QLabel(title,
this);
41 m_titleLabel->setStyleSheet(
"font-weight: bold;");
42 layout->addWidget(m_titleLabel, 1);
45 m_urlLabel =
new QLabel(url,
this);
47 layout->addWidget(m_urlLabel);
52 setAttribute(Qt::WA_Hover);
55CAddressCompleter::CAddressCompleter(QWidget *parent)
57 , m_pLineEdit(nullptr)
58 , m_pShowAnimation(nullptr)
59 , m_pHideAnimation(nullptr)
60 , m_currentSelectedIndex(-1)
61 , m_maxVisibleItems(8)
62 , m_isCompleterVisible(false)
64 m_szEnter = tr(
"Enter '@' show commands") +
"; "
65 + tr(
"Enter a website URL or search content ......");
66 m_szLineEditToolTip = m_szEnter +
"\n\n"
67 + tr(
"Enter ↲ key: Apply current url");
69 m_szListWidgetToolTip += tr(
"Enter ↲ key: Apply current item") +
"\n";
70 m_szListWidgetToolTip += tr(
"Tab ⇆ key: Apply current item") +
"\n";
71 m_szListWidgetToolTip += tr(
"Esc Key: Exit address completer") +
"\n";
72 m_szListWidgetToolTip += tr(
"Space Key: Exit address completer") +
"\n";
73 m_szListWidgetToolTip += tr(
"↑ (Upper arrow) key: Select previous item") +
"\n";
74 m_szListWidgetToolTip += tr(
"↓ (Down arrow) key: Select next item");
76 m_szLineEditToolTipShow = m_szEnter +
"\n\n" + m_szListWidgetToolTip;
77 setToolTip(m_szListWidgetToolTip);
82 m_pSearchTimer =
new QTimer(
this);
84 m_pSearchTimer->setSingleShot(
true);
85 m_pSearchTimer->setInterval(300);
86 bool check = connect(m_pSearchTimer, &QTimer::timeout,
87 this, &CAddressCompleter::performSearch);
92 m_pShowAnimation =
new QPropertyAnimation(
this,
"geometry",
this);
93 m_pHideAnimation =
new QPropertyAnimation(
this,
"geometry",
this);
99CAddressCompleter::~CAddressCompleter()
103void CAddressCompleter::setupUI()
105 setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
106 setAttribute(Qt::WA_TranslucentBackground);
108 QVBoxLayout *mainLayout =
new QVBoxLayout(
this);
109 mainLayout->setContentsMargins(0, 0, 0, 0);
110 mainLayout->setSpacing(0);
112 m_pListWidget =
new QListWidget(
this);
114 m_pListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
115 m_pListWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
116 m_pListWidget->setFocusPolicy(Qt::NoFocus);
118 bool check = connect(m_pListWidget, &QListWidget::itemClicked,
119 this, &CAddressCompleter::onItemClicked);
122 mainLayout->addWidget(m_pListWidget);
124 setLayout(mainLayout);
128 int maxHeight = m_maxVisibleItems * itemHeight + 10;
129 setMaximumHeight(maxHeight);
132void CAddressCompleter::attachToLineEdit(QLineEdit *lineEdit)
135 m_pLineEdit->removeEventFilter(
this);
136 m_pLineEdit->setToolTip(m_szOldLineEditToolTip);
139 m_pLineEdit = lineEdit;
141 m_szOldLineEditToolTip = m_pLineEdit->toolTip();
142 m_pLineEdit->setToolTip(m_szLineEditToolTip);
143 m_pLineEdit->installEventFilter(
this);
144 connect(m_pLineEdit, &QLineEdit::textEdited,
145 this, &CAddressCompleter::onTextChanged);
148 m_pLineEdit->setPlaceholderText(m_szEnter);
152void CAddressCompleter::setMaxVisibleItems(
int count)
154 m_maxVisibleItems = count;
156 int maxHeight = m_maxVisibleItems * itemHeight + 10;
157 setMaximumHeight(maxHeight);
160bool CAddressCompleter::eventFilter(QObject *watched, QEvent *event)
162 if (watched == m_pLineEdit) {
163 switch (event->type()) {
164 case QEvent::KeyPress: {
165 QKeyEvent *keyEvent =
static_cast<QKeyEvent*
>(event);
167 switch (keyEvent->key()) {
169 case Qt::Key_PageDown:
170 if (m_isCompleterVisible) {
177 if (m_isCompleterVisible) {
178 moveToPreviousItem();
184 if (m_isCompleterVisible && m_currentSelectedIndex >= 0) {
189 emit urlSelected(m_pLineEdit->text());
196 if(m_pLineEdit->text().startsWith(
'@'))
201 if (m_isCompleterVisible && m_currentSelectedIndex >= 0) {
209 case QEvent::FocusOut:
211 QTimer::singleShot(100,
this, [
this]() {
213 if (!underMouse() && m_pListWidget && !m_pListWidget->underMouse()) {
223 return QWidget::eventFilter(watched, event);
226void CAddressCompleter::showEvent(QShowEvent *event)
229 m_isCompleterVisible =
true;
233 m_pLineEdit->setFocus();
237void CAddressCompleter::hideEvent(QHideEvent *event)
240 m_isCompleterVisible =
false;
241 m_currentSelectedIndex = -1;
244void CAddressCompleter::onTextChanged(
const QString &text)
246 if (text.isEmpty()) {
253 m_pSearchTimer->start();
256void CAddressCompleter::performSearch()
258 qDebug(log) << Q_FUNC_INFO;
259 QString keyword = m_pLineEdit->text().trimmed();
260 if (keyword.isEmpty() || !m_pListWidget) {
266 m_pListWidget->clear();
267 m_currentSelectedIndex = -1;
270 if(keyword.startsWith(
'@')) {
271 QList<Command> lstCommonds;
272 lstCommonds << Command{tr(
"Search"),
"@search:", QIcon::fromTheme(
"system-search")};
273 lstCommonds << Command{tr(
"Setting"),
"@setting", QIcon::fromTheme(
"system-settings")};
274 lstCommonds << Command{tr(
"History"),
"@history", QIcon()};
275 lstCommonds << Command{tr(
"Bookmarks"),
"@bookmarks", QIcon::fromTheme(
"user-bookmarks")};
278 std::sort(lstCommonds.begin(), lstCommonds.end(), [keyword](Command a, Command b){
280 if (a.cmd.startsWith(keyword) && !b.cmd.startsWith(keyword))
282 if (!a.cmd.startsWith(keyword) && b.cmd.startsWith(keyword))
286 bool aContains = a.cmd.contains(keyword, Qt::CaseInsensitive);
287 bool bContains = b.cmd.contains(keyword, Qt::CaseInsensitive);
288 if (aContains && !bContains) return true;
289 if (!aContains && bContains) return false;
292 return a.cmd < b.cmd;
294 foreach(
auto cmd, lstCommonds) {
295 QListWidgetItem *item =
new QListWidgetItem(m_pListWidget);
296 item->setSizeHint(QSize(0, 40));
297 item->setData(Qt::UserRole, cmd.cmd);
299 cmd.title, cmd.cmd, cmd.icon);
301 m_pListWidget->setItemWidget(item, pCompleterItem);
306 QList<HistoryItem> lstHistory;
307 if(CHistoryDatabase::Instance())
308 lstHistory = CHistoryDatabase::Instance()->searchHistory(keyword);
312 QStringList addedUrls;
314 foreach(
auto i, lstHistory) {
316 QString title = i.title;
319 if (addedUrls.contains(url)) {
324 QListWidgetItem *item =
new QListWidgetItem(m_pListWidget);
325 item->setSizeHint(QSize(0, 40));
329 title.isEmpty() ? url : title,
334 completerItem->setToolTip(title +
"\n" + url +
"\n\n" + toolTip());
335 m_pListWidget->setItemWidget(item, completerItem);
337 item->setData(Qt::UserRole, url);
346 pEdit->setCompletions(addedUrls);
349 if (m_pListWidget->count() == 0) {
350 addSearchSuggestions(keyword);
354 if (m_pListWidget->count() > 0) {
356 if(-1 == m_currentSelectedIndex)
357 m_currentSelectedIndex = 0;
359 m_pListWidget->setCurrentRow(m_currentSelectedIndex);
361 m_pListWidget->scrollToItem(m_pListWidget->item(m_currentSelectedIndex));
369void CAddressCompleter::addSearchSuggestions(
const QString &keyword)
371 qDebug(log) << Q_FUNC_INFO;
376 QString searchText = tr(
"Search \"%1\"").arg(keyword);
378 QListWidgetItem *pSearchItem =
new QListWidgetItem(m_pListWidget);
380 pSearchItem->setSizeHint(QSize(0, 40));
381 pSearchItem->setData(Qt::UserRole, QString(
"@search:%1").arg(keyword));
385 tr(
"Use default search engine"),
386 QIcon(
":/icons/search.png")
389 m_pListWidget->setItemWidget(pSearchItem, pCompleterItem);
393 QStringList commonSites = {
394 "https://www.bing.com/search?q=%1",
395 "https://www.google.com/search?q=%1",
396 "https://www.baidu.com/s?wd=%1",
397 "https://github.com/search?q=%1"
400 for (
const QString &site : commonSites) {
401 QString url = site.arg(keyword);
402 QString displayUrl = site.left(site.indexOf(
"?"));
404 QListWidgetItem *pSiteItem =
new QListWidgetItem(m_pListWidget);
406 pSiteItem->setSizeHint(QSize(0, 40));
407 pSiteItem->setData(Qt::UserRole, url);
410 tr(
"Search in %1").arg(QUrl(displayUrl).host()),
412 getIconForUrl(displayUrl)
414 if(pSiteCompleterItem)
415 m_pListWidget->setItemWidget(pSiteItem, pSiteCompleterItem);
420void CAddressCompleter::onItemClicked(QListWidgetItem *item)
424 QString url = item->data(Qt::UserRole).toString();
427 if (url.startsWith(
"@search:", Qt::CaseInsensitive)) {
428 QString keyword = url.mid(8);
430 emit searchRequested(keyword);
431 }
if(url.startsWith(
"@")) {
432 emit sigCommand(url);
435 emit urlSelected(url);
442 m_pLineEdit->setText(url);
443 m_pLineEdit->setFocus();
447void CAddressCompleter::moveToNextItem()
449 if(!m_pListWidget)
return;
450 int count = m_pListWidget->count();
451 if (count == 0)
return;
453 m_currentSelectedIndex = (m_currentSelectedIndex + 1) % count;
454 m_pListWidget->setCurrentRow(m_currentSelectedIndex);
457 m_pListWidget->scrollToItem(m_pListWidget->item(m_currentSelectedIndex));
460void CAddressCompleter::moveToPreviousItem()
462 if(!m_pListWidget)
return;
463 int count = m_pListWidget->count();
464 if (count == 0)
return;
466 m_currentSelectedIndex = (m_currentSelectedIndex - 1 + count) % count;
467 m_pListWidget->setCurrentRow(m_currentSelectedIndex);
470 m_pListWidget->scrollToItem(m_pListWidget->item(m_currentSelectedIndex));
473void CAddressCompleter::selectCurrentItem()
476 if(!m_pListWidget)
return;
477 QListWidgetItem *item = m_pListWidget->item(m_currentSelectedIndex);
483void CAddressCompleter::showCompleter()
485 if(!m_pListWidget)
return;
486 if (m_isCompleterVisible || m_pListWidget->count() == 0) {
490 m_pLineEdit->setToolTip(m_szLineEditToolTipShow);
491 updateCompleterPosition();
494 QRect startRect = geometry();
495 startRect.setHeight(0);
497 QRect endRect = geometry();
499 int visibleItems = qMin(m_pListWidget->count(), m_maxVisibleItems);
500 int totalHeight = visibleItems * itemHeight + 10;
502 endRect.setHeight(totalHeight);
504 if(m_pShowAnimation) {
505 m_pShowAnimation->setDuration(200);
506 m_pShowAnimation->setStartValue(startRect);
507 m_pShowAnimation->setEndValue(endRect);
508 m_pShowAnimation->setEasingCurve(QEasingCurve::OutCubic);
509 connect(m_pShowAnimation, &QPropertyAnimation::finished,
510 this, &CAddressCompleter::show);
511 m_pShowAnimation->start();
513 setGeometry(endRect);
518void CAddressCompleter::hideCompleter()
520 qDebug(log) << Q_FUNC_INFO;
521 if (!m_isCompleterVisible) {
525 qDebug(log) << Q_FUNC_INFO <<
"end";
527 m_pLineEdit->setToolTip(m_szLineEditToolTip);
530 QRect startRect = geometry();
531 QRect endRect = geometry();
532 endRect.setHeight(0);
534 if(m_pHideAnimation) {
535 m_pHideAnimation->setDuration(150);
536 m_pHideAnimation->setStartValue(startRect);
537 m_pHideAnimation->setEndValue(endRect);
538 m_pHideAnimation->setEasingCurve(QEasingCurve::InCubic);
540 connect(m_pHideAnimation, &QPropertyAnimation::finished,
541 this, &CAddressCompleter::hide);
543 m_pHideAnimation->start();
548void CAddressCompleter::updateCompleterPosition()
550 if (!m_pLineEdit)
return;
553 QPoint globalPos = m_pLineEdit->mapToGlobal(QPoint(0, m_pLineEdit->height()));
556 int width = m_pLineEdit->width();
559 QScreen *screen = QApplication::screenAt(globalPos);
561 QRect screenRect = screen->availableGeometry();
564 int availableHeight = screenRect.bottom() - globalPos.y();
566 int requiredHeight = qMin(m_pListWidget->count(), m_maxVisibleItems) * itemHeight + 10;
568 if (availableHeight < requiredHeight) {
569 globalPos = m_pLineEdit->mapToGlobal(QPoint(0, -requiredHeight));
573 if (globalPos.x() + width > screenRect.right()) {
574 globalPos.setX(screenRect.right() - width);
579 setGeometry(globalPos.x(), globalPos.y(), width, 0);
582QIcon CAddressCompleter::getIconForUrl(
const QString &url)
587 static QIcon defaultIcon;
588 static QIcon httpIcon;
589 static QIcon httpsIcon;
590 static QIcon searchIcon = QIcon::fromTheme(
"system-search");
592 if (url.startsWith(
"https://")) {
594 }
else if (url.startsWith(
"http://")) {
596 }
else if (url.contains(
"@search", Qt::CaseInsensitive)) {