3434#include < QtWidgets/QToolButton>
3535#include < QtWidgets/QScrollBar>
3636#include < QtWidgets/QStatusBar>
37+ #include < QtWidgets/QTreeWidgetItemIterator>
3738#include < QtGui/QIcon>
3839#include < QtGui/QClipboard>
3940#include < QtGui/QDesktopServices>
@@ -54,6 +55,13 @@ EmvViewerMainWindow::EmvViewerMainWindow(
5455 updateTimer->setObjectName (QStringLiteral (" updateTimer" ));
5556 updateTimer->setSingleShot (true );
5657
58+ // Prepare timer used for search input updates. Do this before setupUi()
59+ // calls QMetaObject::connectSlotsByName() to ensure that auto-connect
60+ // works for on_searchTimer_timeout().
61+ searchTimer = new QTimer (this );
62+ searchTimer->setObjectName (QStringLiteral (" searchTimer" ));
63+ searchTimer->setSingleShot (true );
64+
5765 // Setup UI widgets
5866 setupUi (this );
5967 setWindowIcon (QIcon (" :icons/openemv_emv_utils_512x512.png" ));
@@ -83,6 +91,9 @@ EmvViewerMainWindow::EmvViewerMainWindow(
8391 searchLineEdit->setPlaceholderText (tr (" Find in fields..." ));
8492 searchLineEdit->setClearButtonEnabled (true );
8593 treeViewToolBar->addWidget (searchLineEdit);
94+ connect (searchLineEdit, &QLineEdit::textChanged, this , [this ]() {
95+ searchTimer->start (300 );
96+ });
8697
8798 searchPreviousButton = new QToolButton (this );
8899 searchPreviousButton->setObjectName (QStringLiteral (" searchPreviousButton" ));
@@ -96,6 +107,7 @@ EmvViewerMainWindow::EmvViewerMainWindow(
96107 searchPreviousButton->setText (QStringLiteral (" \u2191 " ));
97108 }
98109 treeViewToolBar->addWidget (searchPreviousButton);
110+ connect (searchPreviousButton, &QToolButton::clicked, this , &EmvViewerMainWindow::searchPrevious);
99111
100112 searchNextButton = new QToolButton (this );
101113 searchNextButton->setObjectName (QStringLiteral (" searchNextButton" ));
@@ -109,6 +121,7 @@ EmvViewerMainWindow::EmvViewerMainWindow(
109121 searchNextButton->setText (QStringLiteral (" \u2193 " ));
110122 }
111123 treeViewToolBar->addWidget (searchNextButton);
124+ connect (searchNextButton, &QToolButton::clicked, this , &EmvViewerMainWindow::searchNext);
112125
113126 // Load previous UI values
114127 loadSettings ();
@@ -237,13 +250,148 @@ void EmvViewerMainWindow::updateTreeView()
237250 return ;
238251 }
239252 treeView->populateItems (str);
253+
254+ if (!searchLineEdit->text ().isEmpty ()) {
255+ // Restart search after tree update
256+ startSearch ();
257+ }
258+ }
259+
260+ void EmvViewerMainWindow::startSearch ()
261+ {
262+ // Reset search state
263+ searchMatches.clear ();
264+ currentSearchIndex = -1 ;
265+
266+ QString searchText = searchLineEdit->text ();
267+ if (searchText.isEmpty ()) {
268+ updateSearchStatus ();
269+ return ;
270+ }
271+
272+ // Find all search matches and remember the first index after the
273+ // currently selected item
274+ QTreeWidgetItemIterator itr (treeView, QTreeWidgetItemIterator::NotHidden);
275+ bool rememberNextIndex = false ;
276+ int firstSearchIndex = 0 ;
277+ while (*itr) {
278+ QTreeWidgetItem* item = *itr;
279+ QString itemText = item->text (0 );
280+
281+ if (item == treeView->currentItem ()) {
282+ rememberNextIndex = true ;
283+ }
284+
285+ if (itemText.contains (searchText, Qt::CaseInsensitive)) {
286+ searchMatches.append (item);
287+
288+ if (rememberNextIndex) {
289+ firstSearchIndex = searchMatches.size () - 1 ;
290+ rememberNextIndex = false ;
291+ }
292+ }
293+
294+ ++itr;
295+ }
296+
297+ updateSearchStatus ();
298+
299+ if (!searchMatches.isEmpty ()) {
300+ selectSearchMatch (firstSearchIndex);
301+ }
302+ }
303+
304+ void EmvViewerMainWindow::searchNext ()
305+ {
306+ if (searchMatches.isEmpty ()) {
307+ return ;
308+ }
309+
310+ int nextSearchIndex = currentSearchIndex + 1 ;
311+
312+ if (nextSearchIndex >= searchMatches.size ()) {
313+ nextSearchIndex = 0 ;
314+ }
315+
316+ selectSearchMatch (nextSearchIndex);
317+ }
318+
319+ void EmvViewerMainWindow::searchPrevious ()
320+ {
321+ if (searchMatches.isEmpty ()) {
322+ return ;
323+ }
324+
325+ int prevSearchIndex = currentSearchIndex - 1 ;
326+
327+ if (prevSearchIndex < 0 ) {
328+ prevSearchIndex = searchMatches.size () - 1 ;
329+ }
330+
331+ selectSearchMatch (prevSearchIndex);
332+ }
333+
334+ void EmvViewerMainWindow::selectSearchMatch (int index)
335+ {
336+ QTreeWidgetItem* item;
337+
338+ if (index < 0 || index >= searchMatches.size ()) {
339+ return ;
340+ }
341+ currentSearchIndex = index;
342+ item = searchMatches.at (index);
343+
344+ // Expand parents to make match visible
345+ QTreeWidgetItem* parent = item->parent ();
346+ while (parent) {
347+ parent->setExpanded (true );
348+ parent = parent->parent ();
349+ }
350+
351+ // Scroll to and select match
352+ treeView->scrollToItem (item);
353+ treeView->setCurrentItem (item);
354+
355+ updateSearchStatus ();
356+ }
357+
358+ void EmvViewerMainWindow::updateSearchStatus ()
359+ {
360+ bool hasMatches = !searchMatches.isEmpty ();
361+ bool hasSearchText = !searchLineEdit->text ().isEmpty ();
362+
363+ searchNextButton->setEnabled (hasMatches);
364+ searchPreviousButton->setEnabled (hasMatches);
365+
366+ if (!hasSearchText) {
367+ QMainWindow::statusBar ()->clearMessage ();
368+ } else if (!hasMatches) {
369+ QMainWindow::statusBar ()->showMessage (tr (" No matches found" ));
370+ } else {
371+ QMainWindow::statusBar ()->showMessage (
372+ tr (" Match %1 of %2" ).arg (currentSearchIndex + 1 ).arg (searchMatches.size ())
373+ );
374+ }
375+ }
376+
377+ void EmvViewerMainWindow::clearSearch ()
378+ {
379+ searchLineEdit->clear ();
380+ searchMatches.clear ();
381+ currentSearchIndex = -1 ;
382+ QMainWindow::statusBar ()->clearMessage ();
240383}
241384
242385void EmvViewerMainWindow::on_updateTimer_timeout ()
243386{
244387 updateTreeView ();
245388}
246389
390+ void EmvViewerMainWindow::on_searchTimer_timeout ()
391+ {
392+ startSearch ();
393+ }
394+
247395void EmvViewerMainWindow::on_dataEdit_textChanged ()
248396{
249397 // Rehighlight when text changes. This is required because EmvHighlighter
0 commit comments