ui: Added on-screen search feature (issue #79)

* Use menu action "Edit -> Find in View" or Ctrl + D
* UI displayed as search bar at bottom of main window
* Hide search bar with ECS or close button
* Entering a query searches in name of graph nodes and code contents of code view
* Use Enter to iterate through matches, well move match into view in graph and code view
* Checkboxes allow for adding/removing results for certain views
* Create Bookmark shortcut is now CTRL + S

bug id = 79
This commit is contained in:
Eberhard Graether
2017-09-19 14:00:40 +02:00
parent 4731f379f4
commit 95ef8c3eec
64 changed files with 1642 additions and 174 deletions
+72
View File
@@ -19,6 +19,7 @@
#include "utility/messaging/type/MessageMoveIDECursor.h"
#include "utility/messaging/type/MessageShowErrors.h"
#include "utility/utility.h"
#include "utility/utilityString.h"
#include "qt/element/QtCodeNavigator.h"
#include "qt/utility/QtContextMenu.h"
@@ -163,6 +164,18 @@ void QtCodeArea::lineNumberAreaPaintEvent(QPaintEvent *event)
}
break;
case LOCATION_ERROR:
case LOCATION_SCREEN_SEARCH:
if (annotation.isFocused || annotation.isActive)
{
focus = true;
}
else
{
active = true;
}
break;
case LOCATION_TOKEN:
case LOCATION_SCOPE:
if (annotation.isFocused && utility::shareElement(activeSymbolIds, annotation.tokenIds))
@@ -356,6 +369,65 @@ QRectF QtCodeArea::getLineRectForLineNumber(uint lineNumber) const
return blockBoundingGeometry(block);
}
void QtCodeArea::findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches)
{
const std::string& code = utility::toLowerCase(getCode());
size_t pos = 0;
while (pos != std::string::npos)
{
pos = code.find(query, pos);
if (pos == std::string::npos)
{
break;
}
Annotation matchAnnotation;
matchAnnotation.start = pos;
matchAnnotation.end = pos + query.size();
std::pair<int, int> start = toLineColumn(matchAnnotation.start);
matchAnnotation.startLine = start.first;
matchAnnotation.startCol = start.second;
std::pair<int, int> end = toLineColumn(matchAnnotation.end);
matchAnnotation.endLine = end.first;
matchAnnotation.endCol = end.second;
// Set first 2 bits to 1 to avoid collisions
matchAnnotation.locationId = ~(~Id(0) >> 2) + screenMatches->size() + 1;
matchAnnotation.locationType = LOCATION_SCREEN_SEARCH;
matchAnnotation.isActive = false;
matchAnnotation.isFocused = false;
m_annotations.push_back(matchAnnotation);
screenMatches->push_back(std::make_pair(this, matchAnnotation.locationId));
pos += query.size();
}
if (screenMatches->size() && screenMatches->back().first == this)
{
viewport()->update();
}
}
void QtCodeArea::clearScreenMatches()
{
size_t i = m_annotations.size();
while (i > 0 && m_annotations[i - 1].locationType == LOCATION_SCREEN_SEARCH)
{
i--;
m_linesToRehighlight.push_back(m_annotations[i].startLine - getStartLineNumber());
}
if (i != m_annotations.size())
{
m_annotations.erase(m_annotations.begin() + i, m_annotations.end());
viewport()->update();
}
}
void QtCodeArea::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
+3
View File
@@ -83,6 +83,9 @@ public:
QRectF getLineRectForLineNumber(uint lineNumber) const;
void findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches);
void clearScreenMatches();
protected:
virtual void resizeEvent(QResizeEvent* event) Q_DECL_OVERRIDE;
virtual void mouseReleaseEvent(QMouseEvent* event) Q_DECL_OVERRIDE;
+6 -2
View File
@@ -549,7 +549,7 @@ const QtCodeField::AnnotationColor& QtCodeField::getAnnotationColorForAnnotation
if (!s_annotationColors.size())
{
ColorScheme* scheme = ColorScheme::getInstance().get();
std::vector<std::string> types = { "token", "local_symbol", "scope", "error", "fulltext" };
std::vector<std::string> types = { "token", "local_symbol", "scope", "error", "fulltext_search", "screen_search" };
std::vector<ColorScheme::ColorState> states = { ColorScheme::NORMAL, ColorScheme::FOCUS, ColorScheme::ACTIVE };
for (const std::string& type : types)
@@ -579,10 +579,14 @@ const QtCodeField::AnnotationColor& QtCodeField::getAnnotationColorForAnnotation
{
i = 9;
}
else if (annotation.locationType == LOCATION_FULLTEXT)
else if (annotation.locationType == LOCATION_FULLTEXT_SEARCH)
{
i = 12;
}
else if (annotation.locationType == LOCATION_SCREEN_SEARCH)
{
i = 15;
}
if (annotation.isActive)
{
+1 -1
View File
@@ -99,6 +99,7 @@ protected:
std::vector<Annotation> m_annotations;
std::vector<const Annotation*> m_hoveredAnnotations;
std::vector<int> m_linesToRehighlight;
private:
static std::vector<AnnotationColor> s_annotationColors;
@@ -113,7 +114,6 @@ private:
QtHighlighter* m_highlighter;
std::vector<int> m_lineLengths;
std::vector<int> m_linesToRehighlight;
int m_endTextEditPosition;
};
+23 -6
View File
@@ -143,13 +143,12 @@ QtCodeSnippet* QtCodeFile::addCodeSnippet(const CodeSnippetParams& params)
}
}
m_isCollapsed = false;
std::shared_ptr<QtCodeSnippet> snippet(new QtCodeSnippet(params, m_navigator, this));
if (params.reduced)
{
m_title->setProject(params.title);
m_isCollapsed = false;
}
m_snippetLayout->addWidget(snippet.get());
@@ -172,10 +171,6 @@ QtCodeSnippet* QtCodeFile::addCodeSnippet(const CodeSnippetParams& params)
return m_fileSnippet.get();
}
else
{
m_isCollapsed = false;
}
m_snippets.push_back(snippet);
@@ -229,6 +224,11 @@ QtCodeSnippet* QtCodeFile::insertCodeSnippet(const CodeSnippetParams& params)
QtCodeSnippet* QtCodeFile::getSnippetForLocationId(Id locationId) const
{
if (m_fileSnippet && m_fileSnippet->isVisible() && m_fileSnippet->getLineNumberForLocationId(locationId))
{
return m_fileSnippet.get();
}
for (const std::shared_ptr<QtCodeSnippet>& snippet : m_snippets)
{
if (snippet->getLineNumberForLocationId(locationId))
@@ -438,6 +438,23 @@ void QtCodeFile::updateTitleBar()
m_title->updateTexts();
}
void QtCodeFile::findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches)
{
if (m_fileSnippet && m_fileSnippet->isVisible())
{
m_fileSnippet->findScreenMatches(query, screenMatches);
return;
}
for (const std::shared_ptr<QtCodeSnippet>& snippet : m_snippets)
{
if (snippet->isVisible())
{
snippet->findScreenMatches(query, screenMatches);
}
}
}
void QtCodeFile::clickedMinimizeButton()
{
// overview stats
+3
View File
@@ -14,6 +14,7 @@
class QLabel;
class QPushButton;
class QtCodeArea;
class QtCodeFileTitleButton;
class QtCodeNavigator;
class QtCodeSnippet;
@@ -59,6 +60,8 @@ public:
void updateSnippets();
void updateTitleBar();
void findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches);
public slots:
void clickedMinimizeButton();
void clickedSnippetButton();
@@ -199,6 +199,14 @@ void QtCodeFileList::onWindowFocus()
}
}
void QtCodeFileList::findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches)
{
for (const std::shared_ptr<QtCodeFile>& filePtr : m_files)
{
filePtr->findScreenMatches(query, screenMatches);
}
}
void QtCodeFileList::setFileMinimized(const FilePath path)
{
getFile(path)->setMinimized();
+2
View File
@@ -43,6 +43,8 @@ public:
virtual void onWindowFocus();
virtual void findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches);
void setFileMinimized(const FilePath path);
void setFileSnippets(const FilePath path);
void setFileMaximized(const FilePath path);
@@ -225,6 +225,14 @@ void QtCodeFileSingle::onWindowFocus()
m_title->updateTexts();
}
void QtCodeFileSingle::findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches)
{
if (m_area)
{
m_area->findScreenMatches(query, screenMatches);
}
}
const FilePath& QtCodeFileSingle::getCurrentFilePath() const
{
return m_currentFilePath;
@@ -43,6 +43,8 @@ public:
virtual void onWindowFocus() override;
virtual void findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches) override;
const FilePath& getCurrentFilePath() const;
bool hasFileCached(const FilePath& filePath) const;
@@ -10,6 +10,7 @@
class FilePath;
class QRectF;
class QAbstractScrollArea;
class QtCodeArea;
class QWidget;
class QtCodeNavigateable
@@ -29,6 +30,8 @@ public:
virtual void onWindowFocus() = 0;
virtual void findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches) = 0;
protected:
void ensureWidgetVisibleAnimated(QWidget* parentWidget, QWidget *childWidget, QRectF rect, bool animated, bool onTop);
void ensurePercentVisibleAnimated(double percentA, double percentB, bool animated, bool onTop);
@@ -15,6 +15,7 @@
#include "data/location/SourceLocation.h"
#include "data/location/SourceLocationCollection.h"
#include "data/location/SourceLocationFile.h"
#include "qt/element/QtCodeArea.h"
#include "qt/element/QtCodeFile.h"
#include "qt/element/QtCodeSnippet.h"
#include "qt/utility/utilityQt.h"
@@ -255,6 +256,9 @@ void QtCodeNavigator::clearCodeSnippets()
m_refIndex = 0;
m_singleHasNewFile = false;
m_screenMatches.clear();
m_activeScreenMatchId = 0;
}
void QtCodeNavigator::clearFile()
@@ -549,6 +553,60 @@ void QtCodeNavigator::refreshStyle()
clearCaches();
}
size_t QtCodeNavigator::findScreenMatches(const std::string& query)
{
clearScreenMatches();
m_current->findScreenMatches(query, &m_screenMatches);
return m_screenMatches.size();
}
void QtCodeNavigator::activateScreenMatch(size_t matchIndex)
{
if (matchIndex >= m_screenMatches.size())
{
return;
}
std::pair<QtCodeArea*, Id> p = m_screenMatches[matchIndex];
m_activeScreenMatchId = p.second;
m_currentActiveLocationIds.insert(m_activeScreenMatchId);
p.first->updateContent();
requestScroll(p.first->getSourceLocationFile()->getFilePath(), 0, m_activeScreenMatchId, true, false);
emit scrollRequest();
}
void QtCodeNavigator::deactivateScreenMatch(size_t matchIndex)
{
if (matchIndex >= m_screenMatches.size())
{
return;
}
m_currentActiveLocationIds.erase(m_screenMatches[matchIndex].second);
m_screenMatches[matchIndex].first->updateContent();
m_activeScreenMatchId = 0;
}
void QtCodeNavigator::clearScreenMatches()
{
if (m_activeScreenMatchId)
{
m_currentActiveLocationIds.erase(m_activeScreenMatchId);
m_activeScreenMatchId = 0;
}
for (auto p : m_screenMatches)
{
p.first->clearScreenMatches();
}
m_screenMatches.clear();
}
void QtCodeNavigator::scrollToValue(int value, bool inListMode)
{
if ((m_mode == MODE_LIST) == inListMode)
+8
View File
@@ -83,6 +83,11 @@ public:
void refreshStyle();
size_t findScreenMatches(const std::string& query);
void activateScreenMatch(size_t matchIndex);
void deactivateScreenMatch(size_t matchIndex);
void clearScreenMatches();
void scrollToValue(int value, bool inListMode);
void scrollToLine(const FilePath& filePath, unsigned int line);
void scrollToDefinition(bool animated, bool ignoreActiveReference);
@@ -192,6 +197,9 @@ private:
ScrollRequest m_scrollRequest;
bool m_singleHasNewFile;
std::vector<std::pair<QtCodeArea*, Id>> m_screenMatches;
Id m_activeScreenMatchId = 0;
};
#endif // QT_CODE_NAVIGATOR_H
+5
View File
@@ -187,6 +187,11 @@ std::string QtCodeSnippet::getCode() const
return m_codeArea->getCode();
}
void QtCodeSnippet::findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches)
{
m_codeArea->findScreenMatches(query, screenMatches);
}
void QtCodeSnippet::clickedTitle()
{
if (m_titleId > 0)
+2
View File
@@ -51,6 +51,8 @@ public:
std::string getCode() const;
void findScreenMatches(const std::string& query, std::vector<std::pair<QtCodeArea*, Id>>* screenMatches);
private slots:
void clickedTitle();
void clickedFooter();
@@ -0,0 +1,263 @@
#include "qt/element/QtScreenSearchBox.h"
#include <QApplication>
#include <QCheckBox>
#include <QFocusEvent>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QTimer>
#include "component/controller/helper/ControllerProxy.h"
#include "component/controller/ScreenSearchController.h"
#include "qt/utility/utilityQt.h"
#include "utility/ResourcePaths.h"
QtFocusInFilter::QtFocusInFilter()
{
}
bool QtFocusInFilter::eventFilter(QObject* obj, QEvent* event)
{
QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(obj);
if (lineEdit && event->type() == QEvent::FocusIn && dynamic_cast<QFocusEvent*>(event)->reason() == Qt::MouseFocusReason)
{
emit focusIn();
}
return QObject::eventFilter(obj, event);
}
QtScreenSearchBox::QtScreenSearchBox(ControllerProxy* controllerProxy, QWidget* parent)
: QFrame(parent)
, m_controllerProxy(controllerProxy)
{
setObjectName("screen_search_box");
QHBoxLayout* layout = new QHBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
setLayout(layout);
// search field
{
m_searchButton = new QPushButton();
m_searchButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); // fixes layouting on Mac
m_searchButton->setObjectName("search_button");
layout->addWidget(m_searchButton);
connect(m_searchButton, &QPushButton::clicked, this, &QtScreenSearchBox::setFocus);
m_searchBox = new QLineEdit(this);
m_searchBox->setObjectName("search_box");
m_searchBox->setAttribute(Qt::WA_LayoutUsesWidgetRect); // fixes layouting on Mac
m_searchBox->setAttribute(Qt::WA_MacShowFocusRect, 0); // remove blue focus box on Mac
layout->addWidget(m_searchBox);
connect(m_searchBox, &QLineEdit::textChanged, this, &QtScreenSearchBox::searchQueryChanged);
connect(m_searchBox, &QLineEdit::returnPressed, this, &QtScreenSearchBox::returnPressed);
QtFocusInFilter* filter = new QtFocusInFilter();
m_searchBox->installEventFilter(filter);
connect(filter, &QtFocusInFilter::focusIn, this, &QtScreenSearchBox::findMatches);
}
// match label
{
m_matchLabel = new QPushButton();
m_matchLabel->setAttribute(Qt::WA_LayoutUsesWidgetRect); // fixes layouting on Mac
m_matchLabel->setObjectName("match_label");
layout->addWidget(m_matchLabel);
connect(m_matchLabel, &QPushButton::clicked, this, &QtScreenSearchBox::setFocus);
}
// buttons
{
m_prevButton = new QPushButton();
m_nextButton = new QPushButton();
m_prevButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); // fixes layouting on Mac
m_nextButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); // fixes layouting on Mac
m_prevButton->setObjectName("prev_button");
m_nextButton->setObjectName("next_button");
layout->addWidget(m_prevButton);
layout->addWidget(m_nextButton);
connect(m_prevButton, &QPushButton::clicked, this, &QtScreenSearchBox::previousPressed);
connect(m_nextButton, &QPushButton::clicked, this, &QtScreenSearchBox::nextPressed);
}
// filter
{
m_checkboxLayout = new QHBoxLayout();
layout->addLayout(m_checkboxLayout);
layout->addStretch();
}
// buttons
{
m_closeButton = new QPushButton();
m_closeButton->setObjectName("close_button");
m_closeButton->setAttribute(Qt::WA_LayoutUsesWidgetRect); // fixes layouting on Mac
layout->addWidget(m_closeButton);
connect(m_closeButton, &QPushButton::clicked, [this](){ emit closePressed(); });
}
m_timer = new QTimer(this);
m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, this, &QtScreenSearchBox::findMatches);
refreshStyle();
setMatchCount(0);
}
QtScreenSearchBox::~QtScreenSearchBox()
{
}
void QtScreenSearchBox::refreshStyle()
{
m_searchButton->setIcon(utility::createButtonIcon(
ResourcePaths::getGuiPath().str() + "search_view/images/search.png",
"screen_search/button"
));
m_prevButton->setIcon(utility::createButtonIcon(
ResourcePaths::getGuiPath().str() + "code_view/images/arrow_left.png",
"screen_search/button"
));
m_nextButton->setIcon(utility::createButtonIcon(
ResourcePaths::getGuiPath().str() + "code_view/images/arrow_right.png",
"screen_search/button"
));
m_closeButton->setIcon(utility::createButtonIcon(
ResourcePaths::getGuiPath().str() + "screen_search_view/images/close.png",
"screen_search/button"
));
m_searchButton->setIconSize(QSize(12, 12));
m_prevButton->setIconSize(QSize(12, 12));
m_nextButton->setIconSize(QSize(12, 12));
m_closeButton->setIconSize(QSize(15, 15));
}
void QtScreenSearchBox::setMatchCount(size_t matchCount)
{
m_matchCount = matchCount;
m_matchIndex = 0;
updateMatchLabel();
m_prevButton->setEnabled(matchCount > 0);
m_nextButton->setEnabled(matchCount > 0);
}
void QtScreenSearchBox::setMatchIndex(size_t matchIndex)
{
m_matchIndex = matchIndex;
updateMatchLabel();
}
void QtScreenSearchBox::addResponder(const std::string& name)
{
QCheckBox* box = new QCheckBox(name.c_str());
box->setObjectName("filter_checkbox");
box->setChecked(true);
m_checkBoxes.push_back(box);
m_checkboxLayout->addWidget(box);
connect(box, &QCheckBox::stateChanged, this, &QtScreenSearchBox::findMatches);
}
void QtScreenSearchBox::setFocus()
{
m_searchBox->setFocus();
if (m_searchBox->text().size())
{
m_searchBox->selectAll();
searchQueryChanged();
}
}
void QtScreenSearchBox::searchQueryChanged()
{
m_timer->stop();
m_timer->start(200);
}
void QtScreenSearchBox::findMatches()
{
m_controllerProxy->executeAsTask<ScreenSearchController>(
[this](ScreenSearchController* controller)
{
std::set<std::string> responderNames;
for (QCheckBox* box : m_checkBoxes)
{
if (box->isChecked())
{
responderNames.insert(box->text().toStdString());
}
}
controller->search(m_searchBox->text().toLower().toStdString(), responderNames);
}
);
}
void QtScreenSearchBox::returnPressed()
{
if (Qt::KeyboardModifier::ShiftModifier & QApplication::keyboardModifiers())
{
previousPressed();
}
else
{
nextPressed();
}
}
void QtScreenSearchBox::previousPressed()
{
activateMatch(false);
}
void QtScreenSearchBox::nextPressed()
{
activateMatch(true);
}
void QtScreenSearchBox::activateMatch(bool next)
{
m_controllerProxy->executeAsTask<ScreenSearchController>(
[next, this](ScreenSearchController* controller)
{
controller->activateMatch(next);
}
);
}
void QtScreenSearchBox::updateMatchLabel()
{
QString text;
if (m_matchIndex > 0)
{
text += QString::number(m_matchIndex) + " of ";
}
text += QString::number(m_matchCount) + " match";
if (m_matchCount != 1)
{
text += "es";
}
m_matchLabel->setText(text);
}
@@ -0,0 +1,84 @@
#ifndef QT_SCREEN_SEARCH_BOX_H
#define QT_SCREEN_SEARCH_BOX_H
#include <QFrame>
class ControllerProxy;
class QCheckBox;
class QHBoxLayout;
class QLineEdit;
class QPushButton;
class QTimer;
class QtFocusInFilter
: public QObject
{
Q_OBJECT
public:
QtFocusInFilter();
signals:
void focusIn();
protected:
bool eventFilter(QObject* obj, QEvent* event);
};
class QtScreenSearchBox
: public QFrame
{
Q_OBJECT
public:
QtScreenSearchBox(ControllerProxy* controllerProxy, QWidget* parent = nullptr);
virtual ~QtScreenSearchBox();
void refreshStyle();
void setMatchCount(size_t matchCount);
void setMatchIndex(size_t matchIndex);
void addResponder(const std::string& name);
signals:
void closePressed();
public slots:
void setFocus();
private slots:
void searchQueryChanged();
void findMatches();
void returnPressed();
void previousPressed();
void nextPressed();
private:
void activateMatch(bool next);
void updateMatchLabel();
ControllerProxy* m_controllerProxy;
QLineEdit* m_searchBox;
QPushButton* m_matchLabel;
QPushButton* m_searchButton;
QPushButton* m_prevButton;
QPushButton* m_nextButton;
QPushButton* m_closeButton;
QHBoxLayout* m_checkboxLayout;
std::vector<QCheckBox*> m_checkBoxes;
size_t m_matchCount = 0;
size_t m_matchIndex = 0;
QTimer* m_timer;
};
#endif // QT_SCREEN_SEARCH_BOX_H