VirtualBox

source: vbox/trunk/src/VBox/Frontends/VirtualBox/src/logviewer/UIVMLogViewerSearchWidget.cpp@ 102493

Last change on this file since 102493 was 101092, checked in by vboxsync, 12 months ago

FE/Qt: bugref:6699, bugref:9072. Fixing 'type to search' functionality of log viewer. take 1.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 17.5 KB
Line 
1/* $Id: UIVMLogViewerSearchWidget.cpp 101092 2023-09-12 12:54:09Z vboxsync $ */
2/** @file
3 * VBox Qt GUI - UIVMLogViewer class implementation.
4 */
5
6/*
7 * Copyright (C) 2010-2023 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28/* Qt includes: */
29#include <QCheckBox>
30#include <QComboBox>
31#if defined(RT_OS_SOLARIS)
32# include <QFontDatabase>
33#endif
34#include <QHBoxLayout>
35#include <QVBoxLayout>
36#include <QLabel>
37#include <QLineEdit>
38#include <QPlainTextEdit>
39#include <QTextBlock>
40
41/* GUI includes: */
42#include "QIToolButton.h"
43#include "UIIconPool.h"
44#include "UISearchLineEdit.h"
45#include "UIVMLogPage.h"
46#include "UIVMLogViewerSearchWidget.h"
47#include "UIVMLogViewerWidget.h"
48#ifdef VBOX_WS_MAC
49# include "VBoxUtils-darwin.h"
50#endif
51
52/* Other VBox includes: */
53#include <iprt/assert.h>
54
55UIVMLogViewerSearchWidget::UIVMLogViewerSearchWidget(QWidget *pParent, UIVMLogViewerWidget *pViewer)
56 : UIVMLogViewerPane(pParent, pViewer)
57 , m_pSearchEditor(0)
58 , m_pNextButton(0)
59 , m_pPreviousButton(0)
60 , m_pCaseSensitiveCheckBox(0)
61 , m_pMatchWholeWordCheckBox(0)
62 , m_pHighlightAllCheckBox(0)
63{
64 /* Prepare: */
65 prepareWidgets();
66 prepareConnections();
67 retranslateUi();
68}
69
70void UIVMLogViewerSearchWidget::refreshSearch()
71{
72 /* We start the search from the end of the doc. assuming log's end is more interesting: */
73 if (isVisible())
74 performSearch(BackwardSearch, true);
75 else
76 reset();
77
78 emit sigHighlightingUpdated();
79}
80
81void UIVMLogViewerSearchWidget::reset()
82{
83 m_iSelectedMatchIndex = 0;
84 m_matchLocationVector.clear();
85 m_matchedCursorPosition.clear();
86 if (m_pSearchEditor)
87 m_pSearchEditor->reset();
88 emit sigHighlightingUpdated();
89}
90
91const QVector<float> &UIVMLogViewerSearchWidget::matchLocationVector() const
92{
93 return m_matchLocationVector;
94}
95
96int UIVMLogViewerSearchWidget::matchCount() const
97{
98 return m_matchedCursorPosition.size();
99}
100
101void UIVMLogViewerSearchWidget::hideEvent(QHideEvent *pEvent)
102{
103 /* Get focus-widget: */
104 QWidget *pFocus = QApplication::focusWidget();
105 /* If focus-widget is valid and child-widget of search-panel,
106 * focus next child-widget in line: */
107 if (pFocus && pFocus->parent() == this)
108 focusNextPrevChild(true);
109 /* Call to base-class: */
110 UIVMLogViewerPane::hideEvent(pEvent);
111 reset();
112}
113
114void UIVMLogViewerSearchWidget::sltSearchTextChanged(const QString &strSearchString)
115{
116 /* Enable/disable Next-Previous buttons as per search-string validity: */
117 m_pNextButton->setEnabled(!strSearchString.isEmpty());
118 m_pPreviousButton->setEnabled(!strSearchString.isEmpty());
119
120 /* If search-string is not empty: */
121 if (!strSearchString.isEmpty())
122 {
123 /* Reset the position to force the search restart from the document's end: */
124 performSearch(BackwardSearch, true);
125 emit sigHighlightingUpdated();
126 return;
127 }
128
129 /* If search-string is empty, reset cursor position: */
130 if (!viewer())
131 return;
132
133 QPlainTextEdit *pBrowser = textEdit();
134 if (!pBrowser)
135 return;
136 /* If cursor has selection: */
137 if (pBrowser->textCursor().hasSelection())
138 {
139 /* Get cursor and reset position: */
140 QTextCursor cursor = pBrowser->textCursor();
141 cursor.setPosition(cursor.anchor());
142 pBrowser->setTextCursor(cursor);
143 }
144 m_matchedCursorPosition.clear();
145 m_matchLocationVector.clear();
146 clearHighlighting();
147 emit sigSearchUpdated();
148}
149
150void UIVMLogViewerSearchWidget::sltHighlightAllCheckBox()
151{
152 if (!viewer())
153 return;
154
155 QTextDocument *pDocument = textDocument();
156 if (!pDocument)
157 return;
158
159 if (m_pHighlightAllCheckBox->isChecked())
160 {
161 const QString &searchString = m_pSearchEditor->text();
162 if (searchString.isEmpty())
163 return;
164 highlightAll(searchString);
165 }
166 else
167 clearHighlighting();
168
169 emit sigHighlightingUpdated();
170}
171
172void UIVMLogViewerSearchWidget::sltCaseSentitiveCheckBox()
173{
174 refreshSearch();
175}
176
177void UIVMLogViewerSearchWidget::sltMatchWholeWordCheckBox()
178{
179 refreshSearch();
180}
181
182void UIVMLogViewerSearchWidget::sltSelectNextPreviousMatch()
183{
184 moveSelection(sender() == m_pNextButton);
185}
186
187void UIVMLogViewerSearchWidget::prepareWidgets()
188{
189 QVBoxLayout *pMainLayout = new QVBoxLayout(this);
190 AssertReturnVoid(pMainLayout);
191
192 /* Create search field layout: */
193 QHBoxLayout *pSearchFieldLayout = new QHBoxLayout;
194 AssertReturnVoid(pSearchFieldLayout);
195 pSearchFieldLayout->setContentsMargins(0, 0, 0, 0);
196#ifdef VBOX_WS_MAC
197 pSearchFieldLayout->setSpacing(5);
198#else
199 pSearchFieldLayout->setSpacing(qApp->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing) / 2);
200#endif
201
202 /* Create search-editor: */
203 m_pSearchEditor = new UISearchLineEdit(0 /* parent */);
204 AssertReturnVoid(m_pSearchEditor);
205 m_pSearchEditor->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
206 pSearchFieldLayout->addWidget(m_pSearchEditor);
207
208 /* Create search button layout: */
209 QHBoxLayout *pSearchButtonsLayout = new QHBoxLayout;
210 AssertReturnVoid(pSearchButtonsLayout);
211
212 pSearchButtonsLayout->setContentsMargins(0, 0, 0, 0);
213 pSearchButtonsLayout->setSpacing(0);
214
215 /* Create Previous button: */
216 m_pPreviousButton = new QIToolButton;
217 AssertReturnVoid(m_pPreviousButton);
218 m_pPreviousButton->setIcon(UIIconPool::iconSet(":/log_viewer_search_backward_16px.png"));
219 pSearchButtonsLayout->addWidget(m_pPreviousButton);
220
221 /* Create Next button: */
222 m_pNextButton = new QIToolButton;
223 AssertReturnVoid(m_pNextButton);
224 m_pNextButton->setIcon(UIIconPool::iconSet(":/log_viewer_search_forward_16px.png"));
225 pSearchButtonsLayout->addWidget(m_pNextButton);
226
227 pSearchFieldLayout->addLayout(pSearchButtonsLayout);
228
229 pMainLayout->addLayout(pSearchFieldLayout);
230
231 /* Create case-sensitive check-box: */
232 m_pCaseSensitiveCheckBox = new QCheckBox;
233 AssertReturnVoid(m_pCaseSensitiveCheckBox);
234 pMainLayout->addWidget(m_pCaseSensitiveCheckBox);
235
236 /* Create whole-word check-box: */
237 m_pMatchWholeWordCheckBox = new QCheckBox;
238 AssertReturnVoid(m_pMatchWholeWordCheckBox);
239 setFocusProxy(m_pMatchWholeWordCheckBox);
240 pMainLayout->addWidget(m_pMatchWholeWordCheckBox);
241
242 /* Create highlight-all check-box: */
243 m_pHighlightAllCheckBox = new QCheckBox;
244 AssertReturnVoid(m_pHighlightAllCheckBox);
245 pMainLayout->addWidget(m_pHighlightAllCheckBox);
246
247 pMainLayout->addStretch(1);
248}
249
250void UIVMLogViewerSearchWidget::prepareConnections()
251{
252 connect(m_pSearchEditor, &UISearchLineEdit::textChanged, this, &UIVMLogViewerSearchWidget::sltSearchTextChanged);
253 connect(m_pNextButton, &QIToolButton::clicked, this, &UIVMLogViewerSearchWidget::sltSelectNextPreviousMatch);
254 connect(m_pPreviousButton, &QIToolButton::clicked, this, &UIVMLogViewerSearchWidget::sltSelectNextPreviousMatch);
255
256 connect(m_pHighlightAllCheckBox, &QCheckBox::stateChanged,
257 this, &UIVMLogViewerSearchWidget::sltHighlightAllCheckBox);
258 connect(m_pCaseSensitiveCheckBox, &QCheckBox::stateChanged,
259 this, &UIVMLogViewerSearchWidget::sltCaseSentitiveCheckBox);
260 connect(m_pMatchWholeWordCheckBox, &QCheckBox::stateChanged,
261 this, &UIVMLogViewerSearchWidget::sltMatchWholeWordCheckBox);
262}
263
264void UIVMLogViewerSearchWidget::retranslateUi()
265{
266 UIVMLogViewerPane::retranslateUi();
267
268 m_pSearchEditor->setToolTip(UIVMLogViewerWidget::tr("Enter a search string here"));
269 m_pNextButton->setToolTip(UIVMLogViewerWidget::tr("Search for the next occurrence of the string (F3)"));
270 m_pPreviousButton->setToolTip(UIVMLogViewerWidget::tr("Search for the previous occurrence of the string (Shift+F3)"));
271
272 m_pCaseSensitiveCheckBox->setText(UIVMLogViewerWidget::tr("C&ase Sensitive"));
273 m_pCaseSensitiveCheckBox->setToolTip(UIVMLogViewerWidget::tr("When checked, perform case sensitive search"));
274
275 m_pMatchWholeWordCheckBox->setText(UIVMLogViewerWidget::tr("Ma&tch Whole Word"));
276 m_pMatchWholeWordCheckBox->setToolTip(UIVMLogViewerWidget::tr("When checked, search matches only complete words"));
277
278 m_pHighlightAllCheckBox->setText(UIVMLogViewerWidget::tr("&Highlight All"));
279 m_pHighlightAllCheckBox->setToolTip(UIVMLogViewerWidget::tr("When checked, all occurence of the search text are highlighted"));
280}
281
282void UIVMLogViewerSearchWidget::keyPressEvent(QKeyEvent *pEvent)
283{
284 switch (pEvent->key())
285 {
286 /* Process Enter press as 'search-next',
287 * performed for any search panel widget: */
288 case Qt::Key_Enter:
289 case Qt::Key_Return:
290 {
291 if (pEvent->modifiers() == 0 ||
292 pEvent->modifiers() & Qt::KeypadModifier)
293 {
294 /* Animate click on 'Next' button: */
295 m_pNextButton->animateClick();
296 return;
297 }
298 break;
299 }
300 default:
301 break;
302 }
303 /* Call to base-class: */
304 UIVMLogViewerPane::keyPressEvent(pEvent);
305}
306
307bool UIVMLogViewerSearchWidget::handleSearchRelatedEvents(QObject *pObject, QEvent *pEvent)
308{
309 /* Handle only events sent to viewer(): */
310 if (pObject != viewer())
311 return false;
312
313 /* Depending on event-type: */
314 switch (pEvent->type())
315 {
316 /* Process key press only: */
317 case QEvent::KeyPress:
318 {
319 /* Cast to corresponding key press event: */
320 QKeyEvent *pKeyEvent = static_cast<QKeyEvent*>(pEvent);
321
322 /* Handle F3/Shift+F3 as search next/previous shortcuts: */
323 if (pKeyEvent->key() == Qt::Key_F3)
324 {
325 /* If there is no modifier 'Key-F3' is pressed: */
326 if (pKeyEvent->QInputEvent::modifiers() == 0)
327 {
328 /* Animate click on 'Next' button: */
329 m_pNextButton->animateClick();
330 return true;
331 }
332 /* If there is 'ShiftModifier' 'Shift + Key-F3' is pressed: */
333 else if (pKeyEvent->QInputEvent::modifiers() == Qt::ShiftModifier)
334 {
335 /* Animate click on 'Prev' button: */
336 m_pPreviousButton->animateClick();
337 return true;
338 }
339 }
340 /* Handle Ctrl+F key combination as a shortcut to focus search field: */
341 else if (pKeyEvent->QInputEvent::modifiers() == Qt::ControlModifier &&
342 pKeyEvent->key() == Qt::Key_F)
343 {
344 /* Make sure current log-page is visible: */
345 emit sigShowPane();
346 /* Set focus on search-editor: */
347 m_pSearchEditor->setFocus();
348 return true;
349 }
350 /* Handle alpha-numeric keys to implement the "find as you type" feature: */
351 else if ((pKeyEvent->QInputEvent::modifiers() & ~Qt::ShiftModifier) == 0 &&
352 pKeyEvent->key() >= Qt::Key_Exclam && pKeyEvent->key() <= Qt::Key_AsciiTilde)
353 {
354 /* Make sure current log-page is visible: */
355 emit sigShowPane();
356 /* Set focus on search-editor: */
357 m_pSearchEditor->setFocus();
358 /* Insert the text to search-editor, which triggers the search-operation for new text: */
359 m_pSearchEditor->insert(pKeyEvent->text());
360 return true;
361 }
362 break;
363 }
364 default:
365 break;
366 }
367
368 /* Call to base-class: */
369 return false;
370}
371
372void UIVMLogViewerSearchWidget::showEvent(QShowEvent *pEvent)
373{
374 /* Call to base-class: */
375 UIVMLogViewerPane::showEvent(pEvent);
376 if (m_pSearchEditor)
377 {
378 /* Set focus on search-editor: */
379 m_pSearchEditor->setFocus();
380 /* Select all the text: */
381 m_pSearchEditor->selectAll();
382 m_pSearchEditor->setMatchCount(m_matchedCursorPosition.size());
383 }
384}
385
386void UIVMLogViewerSearchWidget::performSearch(SearchDirection , bool )
387{
388 QPlainTextEdit *pTextEdit = textEdit();
389 if (!pTextEdit)
390 return;
391 QTextDocument *pDocument = textDocument();
392 if (!pDocument)
393 return;
394 if (!m_pSearchEditor)
395 return;
396
397 const QString &searchString = m_pSearchEditor->text();
398 emit sigSearchUpdated();
399
400 if (searchString.isEmpty())
401 return;
402
403 findAll(pDocument, searchString);
404 m_iSelectedMatchIndex = 0;
405 selectMatch(m_iSelectedMatchIndex, searchString);
406 if (m_pSearchEditor)
407 {
408 m_pSearchEditor->setMatchCount(m_matchedCursorPosition.size());
409 m_pSearchEditor->setScrollToIndex(m_matchedCursorPosition.empty() ? -1 : 0);
410 }
411 if (m_pHighlightAllCheckBox->isChecked())
412 highlightAll(searchString);
413}
414
415void UIVMLogViewerSearchWidget::clearHighlighting()
416{
417 QPlainTextEdit *pTextEdit = textEdit();
418 if (pTextEdit)
419 pTextEdit->setExtraSelections(QList<QTextEdit::ExtraSelection>());
420 emit sigHighlightingUpdated();
421}
422
423void UIVMLogViewerSearchWidget::highlightAll(const QString &searchString)
424{
425 clearHighlighting();
426 QPlainTextEdit *pTextEdit = textEdit();
427
428 if (!pTextEdit)
429 return;
430
431 QList<QTextEdit::ExtraSelection> extraSelections;
432 for (int i = 0; i < m_matchedCursorPosition.size(); ++i)
433 {
434 QTextEdit::ExtraSelection selection;
435 QTextCursor cursor = pTextEdit->textCursor();
436 cursor.setPosition(m_matchedCursorPosition[i]);
437 cursor.setPosition(m_matchedCursorPosition[i] + searchString.length(), QTextCursor::KeepAnchor);
438 QTextCharFormat format = cursor.charFormat();
439 format.setBackground(Qt::yellow);
440
441 selection.cursor = cursor;
442 selection.format = format;
443 extraSelections.append(selection);
444 }
445 pTextEdit->setExtraSelections(extraSelections);
446
447}
448
449void UIVMLogViewerSearchWidget::findAll(QTextDocument *pDocument, const QString &searchString)
450{
451 if (!pDocument)
452 return;
453 m_matchedCursorPosition.clear();
454 m_matchLocationVector.clear();
455 if (searchString.isEmpty())
456 return;
457 QTextCursor cursor(pDocument);
458 QTextDocument::FindFlags flags = constructFindFlags(ForwardSearch);
459 int blockCount = pDocument->blockCount();
460 while (!cursor.isNull() && !cursor.atEnd())
461 {
462 cursor = pDocument->find(searchString, cursor, flags);
463
464 if (!cursor.isNull())
465 {
466 m_matchedCursorPosition << cursor.position() - searchString.length();
467 /* The following assumes we have single line blocks only: */
468 int cursorLine = pDocument->findBlock(cursor.position()).blockNumber();
469 if (blockCount != 0)
470 m_matchLocationVector.push_back(cursorLine / static_cast<float>(blockCount));
471 }
472 }
473}
474
475void UIVMLogViewerSearchWidget::selectMatch(int iMatchIndex, const QString &searchString)
476{
477 if (!textEdit())
478 return;
479 if (searchString.isEmpty())
480 return;
481 if (iMatchIndex < 0 || iMatchIndex >= m_matchedCursorPosition.size())
482 return;
483
484 QTextCursor cursor = textEdit()->textCursor();
485 /* Move the cursor to the beginning of the matched string: */
486 cursor.setPosition(m_matchedCursorPosition.at(iMatchIndex), QTextCursor::MoveAnchor);
487 /* Move the cursor to the end of the matched string while keeping the anchor at the begining thus selecting the text: */
488 cursor.setPosition(m_matchedCursorPosition.at(iMatchIndex) + searchString.length(), QTextCursor::KeepAnchor);
489 textEdit()->ensureCursorVisible();
490 textEdit()->setTextCursor(cursor);
491}
492
493void UIVMLogViewerSearchWidget::moveSelection(bool fForward)
494{
495 if (matchCount() == 0)
496 return;
497 if (fForward)
498 m_iSelectedMatchIndex = m_iSelectedMatchIndex >= m_matchedCursorPosition.size() - 1 ? 0 : (m_iSelectedMatchIndex + 1);
499 else
500 m_iSelectedMatchIndex = m_iSelectedMatchIndex <= 0 ? m_matchedCursorPosition.size() - 1 : (m_iSelectedMatchIndex - 1);
501 selectMatch(m_iSelectedMatchIndex, m_pSearchEditor->text());
502 if (m_pSearchEditor)
503 m_pSearchEditor->setScrollToIndex(m_iSelectedMatchIndex);
504}
505
506int UIVMLogViewerSearchWidget::countMatches(QTextDocument *pDocument, const QString &searchString) const
507{
508 if (!pDocument)
509 return 0;
510 if (searchString.isEmpty())
511 return 0;
512 int count = 0;
513 QTextCursor cursor(pDocument);
514 QTextDocument::FindFlags flags = constructFindFlags(ForwardSearch);
515 while (!cursor.isNull() && !cursor.atEnd())
516 {
517 cursor = pDocument->find(searchString, cursor, flags);
518
519 if (!cursor.isNull())
520 ++count;
521 }
522 return count;
523}
524
525QTextDocument::FindFlags UIVMLogViewerSearchWidget::constructFindFlags(SearchDirection eDirection) const
526{
527 QTextDocument::FindFlags findFlags;
528 if (eDirection == BackwardSearch)
529 findFlags = findFlags | QTextDocument::FindBackward;
530 if (m_pCaseSensitiveCheckBox->isChecked())
531 findFlags = findFlags | QTextDocument::FindCaseSensitively;
532 if (m_pMatchWholeWordCheckBox->isChecked())
533 findFlags = findFlags | QTextDocument::FindWholeWords;
534 return findFlags;
535}
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette