VirtualBox

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

Last change on this file was 103923, checked in by vboxsync, 2 months ago

FE/Qt. bugref:10622. Using new UITranslationEventListener in log viewer classes.

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

© 2023 Oracle
ContactPrivacy policyTerms of Use