Add a find tool bar to the Browse Data tab

This adds a find tool bar to the Browse Data tab which allows the user
to search for values in the current table view. It only looks in
non-filtered rows and in non-hidden columns. It respected display
formats and sort order, too. The idea is to provide an additional level
of searching: the first one is done by using the filters and actually
reduces the number of rows in the view; the second level is done by
using the new find tool bar and allows you to look for values in the
remaining rows (or all rows if there is no filter).

See issue #1608.
This commit is contained in:
Martin Kleusberg
2019-09-26 15:51:38 +02:00
parent 091273869d
commit 671cc6d6c6
5 changed files with 400 additions and 18 deletions

View File

@@ -127,6 +127,41 @@ TableBrowser::TableBrowser(QWidget* parent) :
connect(ui->dataTable->verticalHeader(), &QHeaderView::customContextMenuRequested, this, &TableBrowser::showRecordPopupMenu);
connect(ui->dataTable, &ExtendedTableWidget::openFileFromDropEvent, this, &TableBrowser::requestFileOpen);
connect(ui->dataTable, &ExtendedTableWidget::selectedRowsToBeDeleted, this, &TableBrowser::deleteRecord);
// Set up find frame
ui->frameFind->hide();
QShortcut* shortcutHideFindFrame = new QShortcut(QKeySequence("ESC"), ui->editFindExpression);
connect(shortcutHideFindFrame, &QShortcut::activated, ui->buttonFindClose, &QToolButton::click);
connect(ui->actionFind, &QAction::triggered, [this](bool checked) {
if(checked)
{
ui->frameFind->show();
ui->editFindExpression->setFocus();
} else {
ui->buttonFindClose->click();
}
});
connect(ui->editFindExpression, &QLineEdit::returnPressed, ui->buttonFindNext, &QToolButton::click);
connect(ui->editFindExpression, &QLineEdit::textChanged, this, [this]() {
// When the text has changed but neither Return nor F3 or similar nor any buttons were pressed, we want to include the current
// cell in the search as well. This makes sure the selected cell does not jump around every time the text is changed but only
// when the current cell does not match the search expression anymore.
find(ui->editFindExpression->text(), true, true);
});
connect(ui->buttonFindClose, &QToolButton::clicked, this, [this](){
ui->dataTable->setFocus();
ui->frameFind->hide();
ui->actionFind->setChecked(false);
});
connect(ui->buttonFindPrevious, &QToolButton::clicked, this, [this](){
find(ui->editFindExpression->text(), false);
});
connect(ui->buttonFindNext, &QToolButton::clicked, this, [this](){
find(ui->editFindExpression->text(), true);
});
}
TableBrowser::~TableBrowser()
@@ -219,6 +254,7 @@ void TableBrowser::setEnabled(bool enable)
ui->actionRefresh->setEnabled(enable);
ui->actionPrintTable->setEnabled(enable);
ui->editGlobalFilter->setEnabled(enable);
ui->actionFind->setEnabled(enable);
updateInsertDeleteRecordButton();
}
@@ -270,7 +306,7 @@ void TableBrowser::updateTable()
}
statusMessage += tr(". Sum: %1; Average: %2; Min: %3; Max: %4").arg(sum).arg(sum/sel.count()).arg(min).arg(max);
}
};
}
emit statusMessageRequested(statusMessage);
});
}
@@ -1162,3 +1198,53 @@ void TableBrowser::jumpToRow(const sqlb::ObjectIdentifier& table, QString column
ui->dataTable->filterHeader()->setFilter(static_cast<size_t>(column_index-obj->fields.begin()+1), QString("=") + value);
updateTable();
}
void TableBrowser::find(const QString& expr, bool forward, bool include_first)
{
// Get the cell from which the search should be started. If there is a selected cell, use that. If there is no selected cell, start at the first cell.
QModelIndex start;
if(ui->dataTable->selectionModel()->hasSelection())
start = ui->dataTable->selectionModel()->selectedIndexes().front();
else
start = m_browseTableModel->index(0, 0);
// Prepare the match flags with all the search settings
Qt::MatchFlags flags = Qt::MatchWrap;
if(ui->checkFindCaseSensitive->isChecked())
flags |= Qt::MatchCaseSensitive;
if(ui->checkFindWholeCell->isChecked())
flags |= Qt::MatchFixedString;
else
flags |= Qt::MatchContains;
if(ui->checkFindRegEx->isChecked())
flags |= Qt::MatchRegExp;
// Prepare list of columns to search in. We only search in non-hidden rows
std::vector<int> column_list;
sqlb::ObjectIdentifier tableName = currentlyBrowsedTableName();
if(browseTableSettings[tableName].showRowid)
column_list.push_back(0);
for(int i=1;i<m_browseTableModel->columnCount();i++)
{
if(browseTableSettings[tableName].hiddenColumns.contains(i) == false)
column_list.push_back(i);
else if(browseTableSettings[tableName].hiddenColumns[i] == false)
column_list.push_back(i);
}
// Perform the actual search using the model class
const auto match = m_browseTableModel->nextMatch(start, column_list, expr, flags, !forward, include_first);
// Select the next match if we found one
if(match.isValid())
ui->dataTable->setCurrentIndex(match);
// Make the expression control red if no results were found
if(match.isValid() || expr == "")
ui->editFindExpression->setStyleSheet("");
else
ui->editFindExpression->setStyleSheet("QLineEdit {color: white; background-color: rgb(255, 102, 102)}");
}

View File

@@ -150,6 +150,9 @@ private slots:
void browseDataSetTableEncoding(bool forAllTables = false);
void browseDataSetDefaultTableEncoding();
private:
void find(const QString& expr, bool forward, bool include_first = false);
private:
Ui::TableBrowser* ui;
QIntValidator* gotoValidator;

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>552</width>
<height>362</height>
<width>617</width>
<height>400</height>
</rect>
</property>
<property name="windowTitle">
@@ -74,6 +74,8 @@
<addaction name="separator"/>
<addaction name="actionNewRecord"/>
<addaction name="actionDeleteRecord"/>
<addaction name="separator"/>
<addaction name="actionFind"/>
</widget>
</item>
<item>
@@ -125,6 +127,152 @@
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frameFind">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>31</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout">
<property name="leftMargin">
<number>1</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>1</number>
</property>
<property name="bottomMargin">
<number>1</number>
</property>
<property name="spacing">
<number>3</number>
</property>
<item row="0" column="2">
<widget class="QToolButton" name="buttonFindNext">
<property name="toolTip">
<string>Find next match [Enter, F3]</string>
</property>
<property name="whatsThis">
<string>Find next match with wrapping</string>
</property>
<property name="icon">
<iconset resource="icons/icons.qrc">
<normaloff>:/icons/down</normaloff>:/icons/down</iconset>
</property>
<property name="shortcut">
<string>F3</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QToolButton" name="buttonFindPrevious">
<property name="toolTip">
<string>Find previous match [Shift+F3]</string>
</property>
<property name="whatsThis">
<string>Find previous match with mapping</string>
</property>
<property name="icon">
<iconset resource="icons/icons.qrc">
<normaloff>:/icons/up</normaloff>:/icons/up</iconset>
</property>
<property name="shortcut">
<string>Shift+F3</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QCheckBox" name="checkFindRegEx">
<property name="toolTip">
<string>Interpret search pattern as a regular expression</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When checked, the pattern to find is interpreted as a UNIX regular expression. See &lt;a href=&quot;https://en.wikibooks.org/wiki/Regular_Expressions&quot;&gt;Regular Expression in Wikibooks&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Regular Expression</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QCheckBox" name="checkFindWholeCell">
<property name="whatsThis">
<string>The found pattern must be a whole word</string>
</property>
<property name="text">
<string>Whole Cell</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLineEdit" name="editFindExpression">
<property name="contextMenuPolicy">
<enum>Qt::DefaultContextMenu</enum>
</property>
<property name="whatsThis">
<string>Text pattern to find considering the checks in this frame</string>
</property>
<property name="placeholderText">
<string>Find in table</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="8">
<widget class="QToolButton" name="buttonFindClose">
<property name="toolTip">
<string>Close Find Bar</string>
</property>
<property name="text">
<string>Close Find Bar</string>
</property>
<property name="icon">
<iconset resource="icons/icons.qrc">
<normaloff>:/icons/close</normaloff>:/icons/close</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="6">
<spacer name="horizontalSpacer_1">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="checkFindCaseSensitive">
<property name="whatsThis">
<string>The found pattern must match in letter case</string>
</property>
<property name="text">
<string>Case Sensitive</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
@@ -507,6 +655,24 @@
<enum>Qt::WidgetShortcut</enum>
</property>
</action>
<action name="actionFind">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="icons/icons.qrc">
<normaloff>:/icons/find</normaloff>:/icons/find</iconset>
</property>
<property name="text">
<string>Find in cells</string>
</property>
<property name="toolTip">
<string>Open the find tool bar which allows you to search for values in the table view below.</string>
</property>
<property name="shortcut">
<string>Ctrl+F</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
@@ -527,6 +693,14 @@
<tabstops>
<tabstop>comboBrowseTable</tabstop>
<tabstop>dataTable</tabstop>
<tabstop>editGlobalFilter</tabstop>
<tabstop>editFindExpression</tabstop>
<tabstop>buttonFindPrevious</tabstop>
<tabstop>buttonFindNext</tabstop>
<tabstop>checkFindCaseSensitive</tabstop>
<tabstop>checkFindWholeCell</tabstop>
<tabstop>checkFindRegEx</tabstop>
<tabstop>buttonFindClose</tabstop>
<tabstop>buttonBegin</tabstop>
<tabstop>buttonPrevious</tabstop>
<tabstop>buttonNext</tabstop>
@@ -545,8 +719,8 @@
<slot>updateTable()</slot>
<hints>
<hint type="sourcelabel">
<x>118</x>
<y>141</y>
<x>159</x>
<y>31</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
@@ -561,8 +735,8 @@
<slot>navigatePrevious()</slot>
<hints>
<hint type="sourcelabel">
<x>86</x>
<y>539</y>
<x>54</x>
<y>358</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
@@ -577,8 +751,8 @@
<slot>navigateNext()</slot>
<hints>
<hint type="sourcelabel">
<x>183</x>
<y>539</y>
<x>139</x>
<y>358</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
@@ -593,8 +767,8 @@
<slot>navigateGoto()</slot>
<hints>
<hint type="sourcelabel">
<x>365</x>
<y>539</y>
<x>403</x>
<y>360</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
@@ -609,8 +783,8 @@
<slot>navigateGoto()</slot>
<hints>
<hint type="sourcelabel">
<x>506</x>
<y>538</y>
<x>550</x>
<y>360</y>
</hint>
<hint type="destinationlabel">
<x>399</x>
@@ -625,8 +799,8 @@
<slot>navigateEnd()</slot>
<hints>
<hint type="sourcelabel">
<x>223</x>
<y>539</y>
<x>169</x>
<y>358</y>
</hint>
<hint type="destinationlabel">
<x>499</x>
@@ -641,8 +815,8 @@
<slot>navigateBegin()</slot>
<hints>
<hint type="sourcelabel">
<x>50</x>
<y>539</y>
<x>24</x>
<y>358</y>
</hint>
<hint type="destinationlabel">
<x>499</x>
@@ -886,7 +1060,7 @@
</hint>
<hint type="destinationlabel">
<x>326</x>
<y>347</y>
<y>291</y>
</hint>
</hints>
</connection>

View File

@@ -13,6 +13,7 @@
#include <QUrl>
#include <QtConcurrent/QtConcurrentRun>
#include <QProgressDialog>
#include <QRegularExpression>
#include <json.hpp>
#include "RowLoader.h"
@@ -1004,3 +1005,108 @@ void SqliteTableModel::waitUntilIdle () const
{
worker->waitUntilIdle();
}
QModelIndex SqliteTableModel::nextMatch(const QModelIndex& start, const std::vector<int>& column_list, const QString& value, Qt::MatchFlags flags, bool reverse, bool dont_skip_to_next_field) const
{
// Extract flags
bool whole_cell = !(flags & Qt::MatchContains);
bool regex = flags & Qt::MatchRegExp;
Qt::CaseSensitivity case_sensitive = ((flags & Qt::MatchCaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive);
bool wrap = flags & Qt::MatchWrap;
int increment = (reverse ? -1 : 1);
// Prepare the regular expression for regex mode
QRegularExpression reg_exp;
if(regex)
{
reg_exp = QRegularExpression(value, (case_sensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption));
if(!reg_exp.isValid())
return QModelIndex();
if(whole_cell)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0)
reg_exp.setPattern("\\A(" + reg_exp.pattern() + ")\\Z");
#else
reg_exp.setPattern(QRegularExpression::anchoredPattern(reg_exp.pattern()));
#endif
}
}
// Wait until the row count is there
waitUntilIdle();
// Make sure the start position starts in a column from the list of columns to search in
QModelIndex pos = start;
if(std::find(column_list.begin(), column_list.end(), pos.column()) == column_list.end())
{
// If for some weird reason the start index is not in the column list, we simply use the first column of the column list instead
pos = pos.sibling(pos.row(), reverse ? column_list.back() : column_list.front());
}
// Get the last cell to search in. If wrapping is enabled, we search until we hit the start cell again. If wrapping is not enabled, we start at the last
// cell of the table.
QModelIndex end = (wrap ? pos : index(rowCount(), column_list.back()));
// Loop through all cells for the search
while(true)
{
// Go to the next cell and skip all columns in between which we do not care about. This is done as the first step in order
// to skip the start index when matching the first cell is disabled.
if(dont_skip_to_next_field == false)
{
while(true)
{
// Next cell position
int next_row = pos.row();
int next_column = pos.column() + increment;
// Have we reached the end of the row? Then go to the next one
if(next_column < 0 || next_column >= static_cast<int>(m_headers.size()))
{
next_row += increment;
next_column = (reverse ? column_list.back() : column_list.front());
}
// Have we reached the last row? Then wrap around to the first one
if(wrap && (next_row < 0 || next_row >= rowCount()))
next_row = (reverse ? rowCount()-1 : 0);
// Set next index for search
pos = pos.sibling(next_row, next_column);
// Have we hit the last column? We have not found anything then
if(pos == end)
return QModelIndex();
// Is this a column which we are supposed to search in? If so, stop looking for the next cell and start comparing
if(std::find(column_list.begin(), column_list.end(), next_column) != column_list.end())
break;
}
}
// Make sure the next time we hit the above check, we actuall move on to the next cell and do not skip the loop again.
dont_skip_to_next_field = false;
// Get row from cache. If it is not in the cache, load the next chunk from the database
const size_t row = static_cast<size_t>(pos.row());
if(!m_cache.count(row))
{
triggerCacheLoad(static_cast<int>(row));
waitUntilIdle();
}
const Row* row_data = &m_cache.at(row);
// Get cell data
const size_t column = static_cast<size_t>(pos.column());
QString data = row_data->at(column);
// Perform comparison
if(whole_cell && !regex && data.compare(value, case_sensitive) == 0)
return pos;
else if(!whole_cell && !regex && data.contains(value, case_sensitive))
return pos;
else if(regex && reg_exp.match(data).hasMatch())
return pos;
}
}

View File

@@ -117,6 +117,19 @@ public:
void addCondFormat(int column, const CondFormat& condFormat);
void setCondFormats(int column, const std::vector<CondFormat>& condFormats);
// Search for the specified expression in the given cells. This intended as a replacement for QAbstractItemModel::match() even though
// it does not override it, which - because of the different parameters - is not possible.
// start contains the index to start with, column_list contains the ordered list of the columns to look in, value is the value to search for,
// flags allows to modify the search process (Qt::MatchContains, Qt::MatchRegExp, Qt::MatchCaseSensitive, and Qt::MatchWrap are understood),
// reverse can be set to true to progress through the cells in backwards direction, and dont_skip_to_next_field can be set to true if the current
// cell can be matched as well.
QModelIndex nextMatch(const QModelIndex& start,
const std::vector<int>& column_list,
const QString& value,
Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchContains),
bool reverse = false,
bool dont_skip_to_next_field = false) const;
DBBrowserDB& db() { return m_db; }
public slots: