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