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