Rabbit Remote Control 0.1.0-bate16
Loading...
Searching...
No Matches
AddressCompleter.cpp
1// Author: Kang Lin <kl222@126.com>
2
3#include <QStyle>
4#include <QKeyEvent>
5#include <QApplication>
6#include <QScreen>
7#include <QDebug>
8#include <QSqlQuery>
9#include <QSqlError>
10#include <QScrollBar>
11#include <QLoggingCategory>
12#include <algorithm>
13#include "AddressCompleter.h"
14#include "AutoCompleteLineEdit.h"
15#include "HistoryDatabase.h"
16
17static Q_LOGGING_CATEGORY(log, "WebBrowser.Address")
19 const QString &url,
20 const QIcon &icon,
21 QWidget *parent)
22 : QWidget(parent)
23 , m_title(title)
24 , m_url(url)
25{
26 setFixedHeight(40);
27
28 QHBoxLayout *layout = new QHBoxLayout(this);
29 layout->setContentsMargins(8, 4, 8, 4);
30 layout->setSpacing(8);
31
32 // 图标
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);
38
39 // 标题
40 m_titleLabel = new QLabel(title, this);
41 m_titleLabel->setStyleSheet("font-weight: bold;");
42 layout->addWidget(m_titleLabel, 1);
43
44 // URL(灰色显示)
45 m_urlLabel = new QLabel(url, this);
46 //m_urlLabel->setStyleSheet("color: gray; font-size: 11px;");
47 layout->addWidget(m_urlLabel);
48
49 setLayout(layout);
50
51 // 鼠标悬停效果
52 setAttribute(Qt::WA_Hover);
53}
54
55CAddressCompleter::CAddressCompleter(CHistoryDatabase *db, QWidget *parent)
56 : 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)
63 , m_pDatabase(db)
64 , m_bIsWayland(false)
65{
66 m_szEnter = tr("Enter '@' show commands") + "; "
67 + tr("Enter a website URL or search content ......");
68 m_szLineEditToolTip = m_szEnter + "\n\n"
69 + tr("Enter ↲ key: Apply current url");
70
71 m_szListWidgetToolTip += tr("Enter ↲ key: Apply current item") + "\n";
72 m_szListWidgetToolTip += tr("Tab ⇆ key: Apply current item") + "\n";
73 m_szListWidgetToolTip += tr("Esc Key: Exit address completer") + "\n";
74 m_szListWidgetToolTip += tr("Space Key: Exit address completer") + "\n";
75 m_szListWidgetToolTip += tr("↑ (Upper arrow) key: Select previous item") + "\n";
76 m_szListWidgetToolTip += tr("↓ (Down arrow) key: Select next item");
77
78 m_szLineEditToolTipShow = m_szEnter + "\n\n" + m_szListWidgetToolTip;
79 setToolTip(m_szListWidgetToolTip);
80
81#if defined(Q_OS_LINUX)
82 if(-1 == QGuiApplication::platformName().indexOf(QRegularExpression("wayland.*")))
83 m_bIsWayland = false;
84 else
85 m_bIsWayland = true;
86#endif
87
88 setupUI();
89
90 // 设置搜索延迟定时器(300ms防抖动)
91 m_pSearchTimer = new QTimer(this);
92 if(m_pSearchTimer) {
93 m_pSearchTimer->setSingleShot(true);
94 m_pSearchTimer->setInterval(300);
95 bool check = connect(m_pSearchTimer, &QTimer::timeout,
96 this, &CAddressCompleter::performSearch);
97 Q_ASSERT(check);
98 }
99
100 // 动画效果
101 m_pShowAnimation = new QPropertyAnimation(this, "geometry", this);
102 m_pHideAnimation = new QPropertyAnimation(this, "geometry", this);
103
104 // 初始隐藏
105 hide();
106}
107
108CAddressCompleter::~CAddressCompleter()
109{
110}
111
112void CAddressCompleter::setupUI()
113{
114 Qt::WindowFlags flags;
115 flags = Qt::FramelessWindowHint | Qt::CustomizeWindowHint;
116 if(!m_bIsWayland) {
117 flags |= Qt::Tool;
118 }
119 setWindowFlags(flags);
120 setAttribute(Qt::WA_TranslucentBackground);
121
122 QVBoxLayout *mainLayout = new QVBoxLayout(this);
123 mainLayout->setContentsMargins(0, 0, 0, 0);
124 mainLayout->setSpacing(0);
125
126 m_pListWidget = new QListWidget(this);
127 if(m_pListWidget) {
128 m_pListWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
129 m_pListWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
130 m_pListWidget->setFocusPolicy(Qt::NoFocus);
131
132 bool check = connect(m_pListWidget, &QListWidget::itemClicked,
133 this, &CAddressCompleter::onItemClicked);
134 Q_ASSERT(check);
135
136 mainLayout->addWidget(m_pListWidget);
137 }
138 setLayout(mainLayout);
139
140 // 设置最大高度
141 int itemHeight = 40;
142 int maxHeight = m_maxVisibleItems * itemHeight + 10; // 10是边框和内边距
143 setMaximumHeight(maxHeight);
144}
145
146void CAddressCompleter::attachToLineEdit(QLineEdit *lineEdit)
147{
148 if (m_pLineEdit) {
149 m_pLineEdit->removeEventFilter(this);
150 m_pLineEdit->setToolTip(m_szOldLineEditToolTip);
151 }
152
153 m_pLineEdit = lineEdit;
154 if (m_pLineEdit) {
155 m_szOldLineEditToolTip = m_pLineEdit->toolTip();
156 m_pLineEdit->setToolTip(m_szLineEditToolTip);
157 m_pLineEdit->installEventFilter(this);
158 connect(m_pLineEdit, &QLineEdit::textEdited,
159 this, &CAddressCompleter::onTextChanged);
160
161 // 设置提示文本
162 m_pLineEdit->setPlaceholderText(m_szEnter);
163 }
164}
165
166void CAddressCompleter::setMaxVisibleItems(int count)
167{
168 m_maxVisibleItems = count;
169 int itemHeight = 40;
170 int maxHeight = m_maxVisibleItems * itemHeight + 10;
171 setMaximumHeight(maxHeight);
172}
173
174bool CAddressCompleter::eventFilter(QObject *watched, QEvent *event)
175{
176 if (watched == m_pLineEdit) {
177 switch (event->type()) {
178 case QEvent::KeyPress: {
179 QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
180 //qDebug(log) << Q_FUNC_INFO << keyEvent;
181 switch (keyEvent->key()) {
182 case Qt::Key_Down:
183 case Qt::Key_PageDown:
184 if (m_isCompleterVisible) {
185 moveToNextItem();
186 return true;
187 }
188 break;
189 case Qt::Key_Up:
190 case Qt::Key_PageUp:
191 if (m_isCompleterVisible) {
192 moveToPreviousItem();
193 return true;
194 }
195 break;
196 case Qt::Key_Return:
197 case Qt::Key_Enter:
198 if (m_isCompleterVisible && m_currentSelectedIndex >= 0) {
199 selectCurrentItem();
200 return true;
201 }
202 if(m_pLineEdit) {
203 emit urlSelected(m_pLineEdit->text());
204 }
205 break;
206 case Qt::Key_Escape:
207 hideCompleter();
208 break;
209 case Qt::Key_Space:
210 if(m_pLineEdit->text().startsWith('@'))
211 break;
212 hideCompleter();
213 return true;
214 case Qt::Key_Tab:
215 if (m_isCompleterVisible && m_currentSelectedIndex >= 0) {
216 selectCurrentItem();
217 return true;
218 }
219 break;
220 }
221 break;
222 }
223 case QEvent::FocusOut:
224 // 延迟隐藏
225 QTimer::singleShot(100, this, [this]() {
226 //qDebug(log) << "lineedit focus out";
227 if (!underMouse() && m_pListWidget && !m_pListWidget->underMouse()) {
228 hideCompleter();
229 }
230 });
231 break;
232 default:
233 break;
234 }
235 }
236
237 return QWidget::eventFilter(watched, event);
238}
239
240void CAddressCompleter::showEvent(QShowEvent *event)
241{
242 Q_UNUSED(event);
243 m_isCompleterVisible = true;
244
245 // 确保焦点在输入框
246 if (m_pLineEdit) {
247 m_pLineEdit->setFocus();
248 }
249}
250
251void CAddressCompleter::hideEvent(QHideEvent *event)
252{
253 Q_UNUSED(event);
254 m_isCompleterVisible = false;
255 m_currentSelectedIndex = -1;
256}
257
258void CAddressCompleter::onTextChanged(const QString &text)
259{
260 if (text.isEmpty()) {
261 hideCompleter();
262 return;
263 }
264
265 // 重启定时器(防抖动)
266 if(m_pSearchTimer)
267 m_pSearchTimer->start();
268}
269
270void CAddressCompleter::performSearch()
271{
272 qDebug(log) << Q_FUNC_INFO;
273 QString keyword = m_pLineEdit->text().trimmed();
274 if (keyword.isEmpty() || !m_pListWidget) {
275 hideCompleter();
276 return;
277 }
278
279 // 清空现有项
280 m_pListWidget->clear();
281 m_currentSelectedIndex = -1;
282
283 // 增加 “@” 命令
284 if(keyword.startsWith('@')) {
285 QList<Command> lstCommonds;
286 lstCommonds << Command{tr("Search"), "@search:", QIcon::fromTheme("system-search")};
287 lstCommonds << Command{tr("Setting"), "@setting", QIcon::fromTheme("system-settings")};
288 lstCommonds << Command{tr("History"), "@history", QIcon()};
289 lstCommonds << Command{tr("Bookmarks"), "@bookmarks", QIcon::fromTheme("user-bookmarks")};
290
291 // 根据 keyword 进行排序
292 std::sort(lstCommonds.begin(), lstCommonds.end(), [keyword](Command a, Command b){
293 // 1. 完全匹配的排在最前面
294 if (a.cmd.startsWith(keyword) && !b.cmd.startsWith(keyword))
295 return true;
296 if (!a.cmd.startsWith(keyword) && b.cmd.startsWith(keyword))
297 return false;
298
299 // 2. 包含关键字的排在前面
300 bool aContains = a.cmd.contains(keyword, Qt::CaseInsensitive);
301 bool bContains = b.cmd.contains(keyword, Qt::CaseInsensitive);
302 if (aContains && !bContains) return true;
303 if (!aContains && bContains) return false;
304
305 // 3. 否则按字母顺序排序
306 return a.cmd < b.cmd;
307 });
308 foreach(auto cmd, lstCommonds) {
309 QListWidgetItem *item = new QListWidgetItem(m_pListWidget);
310 item->setSizeHint(QSize(0, 40));
311 item->setData(Qt::UserRole, cmd.cmd);
312 CAddressCompleterItem *pCompleterItem = new CAddressCompleterItem(
313 cmd.title, cmd.cmd, cmd.icon);
314 if(pCompleterItem)
315 m_pListWidget->setItemWidget(item, pCompleterItem);
316 }
317 }
318
319 // 搜索历史记录
320 QList<HistoryItem> lstHistory;
321 if(m_pDatabase)
322 lstHistory = m_pDatabase->searchHistory(keyword);
323
324 // 添加搜索结果
325 int count = 0;
326 QStringList addedUrls; // 用于去重
327
328 foreach(auto i, lstHistory) {
329 QString url = i.url;
330 QString title = i.title;
331
332 // 去重
333 if (addedUrls.contains(url)) {
334 continue;
335 }
336
337 // 创建自定义项
338 QListWidgetItem *item = new QListWidgetItem(m_pListWidget);
339 item->setSizeHint(QSize(0, 40));
340
341 // 创建自定义widget
342 CAddressCompleterItem *completerItem = new CAddressCompleterItem(
343 title.isEmpty() ? url : title,
344 url,
345 i.icon
346 );
347 if(completerItem) {
348 completerItem->setToolTip(title + "\n" + url + "\n\n" + toolTip());
349 m_pListWidget->setItemWidget(item, completerItem);
350 }
351 item->setData(Qt::UserRole, url);
352
353 addedUrls << url;
354
355 count++;
356 }
357
358 CAutoCompleteLineEdit* pEdit = qobject_cast<CAutoCompleteLineEdit*>(m_pLineEdit);
359 if(pEdit)
360 pEdit->setCompletions(addedUrls);
361
362 // 如果没有找到历史记录,显示搜索建议
363 if (m_pListWidget->count() == 0) {
364 addSearchSuggestions(keyword);
365 }
366
367 // 如果有结果,显示下拉列表
368 if (m_pListWidget->count() > 0) {
369 // 选中第一项
370 if(-1 == m_currentSelectedIndex)
371 m_currentSelectedIndex = 0;
372
373 m_pListWidget->setCurrentRow(m_currentSelectedIndex);
374 // 确保选中项可见
375 m_pListWidget->scrollToItem(m_pListWidget->item(m_currentSelectedIndex));
376
377 showCompleter();
378 } else {
379 hideCompleter();
380 }
381}
382
383void CAddressCompleter::addSearchSuggestions(const QString &keyword)
384{
385 qDebug(log) << Q_FUNC_INFO;
386 if(!m_pListWidget)
387 return;
388
389 // 添加搜索建议
390 QString searchText = tr("Search \"%1\"").arg(keyword);
391
392 QListWidgetItem *pSearchItem = new QListWidgetItem(m_pListWidget);
393 if(pSearchItem) {
394 pSearchItem->setSizeHint(QSize(0, 40));
395 pSearchItem->setData(Qt::UserRole, QString("@search:%1").arg(keyword));
396
397 CAddressCompleterItem *pCompleterItem = new CAddressCompleterItem(
398 searchText,
399 tr("Use default search engine"),
400 QIcon(":/icons/search.png")
401 );
402 if(pCompleterItem)
403 m_pListWidget->setItemWidget(pSearchItem, pCompleterItem);
404 }
405
406 // 添加常用网站建议
407 QStringList commonSites = {
408 "https://www.bing.com/search?q=%1",
409 "https://www.google.com/search?q=%1",
410 "https://www.baidu.com/s?wd=%1",
411 "https://github.com/search?q=%1"
412 };
413
414 for (const QString &site : commonSites) {
415 QString url = site.arg(keyword);
416 QString displayUrl = site.left(site.indexOf("?"));
417
418 QListWidgetItem *pSiteItem = new QListWidgetItem(m_pListWidget);
419 if(pSiteItem) {
420 pSiteItem->setSizeHint(QSize(0, 40));
421 pSiteItem->setData(Qt::UserRole, url);
422
423 CAddressCompleterItem *pSiteCompleterItem = new CAddressCompleterItem(
424 tr("Search in %1").arg(QUrl(displayUrl).host()),
425 url,
426 getIconForUrl(displayUrl)
427 );
428 if(pSiteCompleterItem)
429 m_pListWidget->setItemWidget(pSiteItem, pSiteCompleterItem);
430 }
431 }
432}
433
434void CAddressCompleter::onItemClicked(QListWidgetItem *item)
435{
436 if (!item) return;
437
438 QString url = item->data(Qt::UserRole).toString();
439
440 // 处理搜索请求
441 if (url.startsWith("@search:", Qt::CaseInsensitive)) {
442 QString keyword = url.mid(8);
443 //qDebug(log) << "emit searchRequested:" << keyword;
444 emit searchRequested(keyword);
445 } if(url.startsWith("@")) {
446 emit sigCommand(url);
447 }else {
448 //qDebug(log) << "emit urlSelected:" << url;
449 emit urlSelected(url);
450 }
451
452 hideCompleter();
453
454 // 将URL填入地址栏
455 if (m_pLineEdit) {
456 m_pLineEdit->setText(url);
457 m_pLineEdit->setFocus();
458 }
459}
460
461void CAddressCompleter::moveToNextItem()
462{
463 if(!m_pListWidget) return;
464 int count = m_pListWidget->count();
465 if (count == 0) return;
466
467 m_currentSelectedIndex = (m_currentSelectedIndex + 1) % count;
468 m_pListWidget->setCurrentRow(m_currentSelectedIndex);
469
470 // 确保选中项可见
471 m_pListWidget->scrollToItem(m_pListWidget->item(m_currentSelectedIndex));
472}
473
474void CAddressCompleter::moveToPreviousItem()
475{
476 if(!m_pListWidget) return;
477 int count = m_pListWidget->count();
478 if (count == 0) return;
479
480 m_currentSelectedIndex = (m_currentSelectedIndex - 1 + count) % count;
481 m_pListWidget->setCurrentRow(m_currentSelectedIndex);
482
483 // 确保选中项可见
484 m_pListWidget->scrollToItem(m_pListWidget->item(m_currentSelectedIndex));
485}
486
487void CAddressCompleter::selectCurrentItem()
488{
489 //qDebug(log) << Q_FUNC_INFO;
490 if(!m_pListWidget) return;
491 QListWidgetItem *item = m_pListWidget->item(m_currentSelectedIndex);
492 if (item) {
493 onItemClicked(item);
494 }
495}
496
497void CAddressCompleter::showCompleter()
498{
499 if(!m_pListWidget) return;
500 if (m_isCompleterVisible || m_pListWidget->count() == 0) {
501 return;
502 }
503
504 m_pLineEdit->setToolTip(m_szLineEditToolTipShow);
505 updateCompleterPosition();
506
507 // 动画显示
508 QRect startRect = geometry();
509 startRect.setHeight(0);
510
511 QRect endRect = geometry();
512 int itemHeight = 40;
513 int visibleItems = qMin(m_pListWidget->count(), m_maxVisibleItems);
514 int totalHeight = visibleItems * itemHeight + 10;
515
516 endRect.setHeight(totalHeight);
517
518 if(m_pShowAnimation) {
519 m_pShowAnimation->setDuration(200);
520 m_pShowAnimation->setStartValue(startRect);
521 m_pShowAnimation->setEndValue(endRect);
522 m_pShowAnimation->setEasingCurve(QEasingCurve::OutCubic);
523 connect(m_pShowAnimation, &QPropertyAnimation::finished,
524 this, [this]() {
525 show();
526 raise();
527 });
528 m_pShowAnimation->start();
529 } else {
530 setGeometry(endRect);
531 show();
532 raise();
533 }
534}
535
536void CAddressCompleter::hideCompleter()
537{
538 qDebug(log) << Q_FUNC_INFO;
539 if (!m_isCompleterVisible) {
540 return;
541 }
542
543 qDebug(log) << Q_FUNC_INFO << "end";
544
545 m_pLineEdit->setToolTip(m_szLineEditToolTip);
546
547 // 动画隐藏
548 QRect startRect = geometry();
549 QRect endRect = geometry();
550 endRect.setHeight(0);
551
552 if(m_pHideAnimation) {
553 m_pHideAnimation->setDuration(150);
554 m_pHideAnimation->setStartValue(startRect);
555 m_pHideAnimation->setEndValue(endRect);
556 m_pHideAnimation->setEasingCurve(QEasingCurve::InCubic);
557
558 connect(m_pHideAnimation, &QPropertyAnimation::finished,
559 this, &CAddressCompleter::hide);
560
561 m_pHideAnimation->start();
562 } else
563 hide();
564}
565
566void CAddressCompleter::updateCompleterPosition()
567{
568 if (!m_pLineEdit) return;
569
570 // 获取输入框的全局位置
571 QPoint globalPos = m_pLineEdit->mapToGlobal(QPoint(0, m_pLineEdit->height()));
572
573 // 设置宽度与输入框相同
574 int width = m_pLineEdit->width();
575
576 // 检查是否超出屏幕
577 QScreen *screen = QApplication::screenAt(globalPos);
578 if (screen) {
579 QRect screenRect = screen->availableGeometry();
580
581 // 如果下拉框超出屏幕底部,显示在输入框上方
582 int availableHeight = screenRect.bottom() - globalPos.y();
583 int itemHeight = 40;
584 int requiredHeight = qMin(m_pListWidget->count(), m_maxVisibleItems) * itemHeight + 10;
585
586 if (availableHeight < requiredHeight) {
587 globalPos = m_pLineEdit->mapToGlobal(QPoint(0, -requiredHeight));
588 }
589
590 // 确保不超出屏幕右侧
591 if (globalPos.x() + width > screenRect.right()) {
592 globalPos.setX(screenRect.right() - width);
593 }
594 }
595
596 if(m_bIsWayland) {
597 QWidget* pWin = qobject_cast<QWidget*>(parent());
598 if(pWin) {
599 globalPos = pWin->mapFromGlobal(globalPos);
600 } else {
601 qCritical(log) << "Current platform plugin is wayland. the parent must be set.";
602 }
603 }
604
605 // 设置位置和大小
606 setGeometry(globalPos.x(), globalPos.y(), width, 0);
607}
608
609QIcon CAddressCompleter::getIconForUrl(const QString &url)
610{
611 // TODO: 这里可以根据URL返回不同的图标
612 // 简化实现:返回默认图标
613
614 static QIcon defaultIcon;
615 static QIcon httpIcon;
616 static QIcon httpsIcon;
617 static QIcon searchIcon = QIcon::fromTheme("system-search");
618
619 if (url.startsWith("https://")) {
620 return httpsIcon;
621 } else if (url.startsWith("http://")) {
622 return httpIcon;
623 } else if (url.contains("@search", Qt::CaseInsensitive)) {
624 return searchIcon;
625 }
626
627 return defaultIcon;
628}
Browser address bar autocomplete feature.
The CHistoryDatabase class.