Files
Sourcetrail/src/app/qt/element/QtSmartSearchBox.cpp
T
Eberhard Graether af1462f955 ui: fixed autocompletion issues
* click in autocompletions list causes search
* right arrow for autocompletion
* changed query operator has from '>' to '.' and operator sub from '.' to '+'
* fixed autocompletions to fit to query
and
* fixed spectral layouting endless looping on unconnected node
2015-02-06 00:51:02 +01:00

761 lines
15 KiB
C++

#include "qt/element/QtSmartSearchBox.h"
#include <stdlib.h>
#include <QApplication>
#include <QClipboard>
#include <QKeyEvent>
#include "utility/messaging/type/MessageSearch.h"
#include "utility/messaging/type/MessageSearchAutocomplete.h"
#include "utility/text/TextAccess.h"
#include "utility/utilityString.h"
#include "data/query/QueryTree.h"
#include "qt/element/QtAutocompletionList.h"
QtQueryElement::QtQueryElement(const QString& text, QWidget* parent)
: QPushButton(text, parent)
{
show();
setCheckable(true);
connect(this, SIGNAL(clicked(bool)), this, SLOT(onChecked(bool)));
}
void QtQueryElement::onChecked(bool)
{
emit wasChecked(this);
}
QtSmartSearchBox::QtSmartSearchBox(QWidget* parent)
: QLineEdit(parent)
, m_allowTextChange(false)
, m_cursorIndex(0)
, m_shiftKeyDown(false)
, m_mousePressed(false)
{
m_highlightRect = new QWidget(this);
m_highlightRect->setGeometry(0, 0, 0, 0);
m_highlightRect->setObjectName("search_box_highlight");
connect(this, SIGNAL(textEdited(const QString&)), this, SLOT(onTextEdited(const QString&)));
connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(onTextChanged(const QString&)));
QCompleter* completer = new QtAutocompletionList(this);
setCompleter(completer);
updatePlaceholder();
}
QtSmartSearchBox::~QtSmartSearchBox()
{
}
void QtSmartSearchBox::setAutocompletionList(const std::vector<SearchMatch>& autocompletionList)
{
QtAutocompletionList* completer = dynamic_cast<QtAutocompletionList*>(this->completer());
completer->completeAt(QPoint(textMargins().left() + 3, height() + 3), autocompletionList);
connect(completer, SIGNAL(matchHighlighted(const SearchMatch&)), this, SLOT(onAutocompletionHighlighted(const SearchMatch&)), Qt::DirectConnection);
connect(completer, SIGNAL(matchActivated(const SearchMatch&)), this, SLOT(onAutocompletionActivated(const SearchMatch&)), Qt::DirectConnection);
if (autocompletionList.size())
{
m_highlightedMatch = *completer->getSearchMatchAt(0);
}
}
void QtSmartSearchBox::setQuery(const std::string& text)
{
clearLineEdit();
m_tokens = QueryTree::tokenizeQuery(text);
m_cursorIndex = m_tokens.size();
updateElements();
}
void QtSmartSearchBox::setFocus()
{
QLineEdit::setFocus(Qt::ShortcutFocusReason);
selectAllElementsWith(true);
layoutElements();
}
void QtSmartSearchBox::search()
{
editTextToElement();
MessageSearch(utility::join(m_tokens, "") + text().toStdString()).dispatch();
}
bool QtSmartSearchBox::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Tab)
{
if (completer()->popup()->isVisible())
{
searchMatchToToken(m_highlightedMatch);
}
else
{
requestAutoCompletions();
}
return true;
}
}
return QWidget::event(event);
}
void QtSmartSearchBox::resizeEvent(QResizeEvent* event)
{
QLineEdit::resizeEvent(event);
layoutElements();
}
void QtSmartSearchBox::keyPressEvent(QKeyEvent* event)
{
m_shiftKeyDown = event->modifiers() & Qt::ShiftModifier;
if (event->key() == Qt::Key_Return)
{
if (!completer()->popup()->isVisible())
{
search();
}
}
else if (event->key() == Qt::Key_Backspace)
{
if (hasSelectedElements())
{
deleteSelectedElements();
return;
}
else if (!hasSelectedText() && cursorPosition() == 0 && m_cursorIndex > 0)
{
m_elements[m_cursorIndex - 1]->setChecked(true);
deleteSelectedElements();
return;
}
}
else if (event->matches(QKeySequence::MoveToPreviousChar))
{
if (hasSelectedElements())
{
for (size_t i = 0; i < m_elements.size(); i++)
{
if (m_elements[i]->isChecked())
{
m_cursorIndex = i;
break;
}
}
selectAllElementsWith(false);
layoutElements();
}
else if (cursorPosition() == 0 && m_cursorIndex > 0)
{
editTextToElement();
moveCursor(-1);
return;
}
}
else if (event->matches(QKeySequence::MoveToNextChar))
{
if (hasSelectedElements())
{
for (size_t i = m_elements.size(); i > 0; i--)
{
if (m_elements[i - 1]->isChecked())
{
m_cursorIndex = i;
break;
}
}
selectAllElementsWith(false);
layoutElements();
}
else if (cursorPosition() == text().size())
{
if (completer()->popup()->isVisible())
{
searchMatchToToken(m_highlightedMatch);
}
else if (!editTextToElement())
{
moveCursor(1);
}
return;
}
}
else if (event->matches(QKeySequence::SelectPreviousChar))
{
if (cursorPosition() == 0 && m_cursorIndex > 0)
{
editTextToElement();
m_elements[m_cursorIndex - 1]->setChecked(!m_elements[m_cursorIndex - 1]->isChecked());
moveCursor(-1);
}
}
else if (event->matches(QKeySequence::SelectNextChar))
{
if (cursorPosition() == text().size() && m_cursorIndex < m_elements.size())
{
editTextToElement();
m_elements[m_cursorIndex]->setChecked(!m_elements[m_cursorIndex]->isChecked());
moveCursor(1);
}
}
else if (event->matches(QKeySequence::MoveToStartOfLine))
{
editTextToElement();
moveCursorTo(0);
return;
}
else if (event->matches(QKeySequence::MoveToEndOfLine))
{
if (m_cursorIndex < m_elements.size())
{
editTextToElement();
moveCursorTo(m_elements.size());
return;
}
}
else if (event->matches(QKeySequence::SelectAll))
{
if (m_elements.size())
{
editTextToElement();
selectAllElementsWith(true);
layoutElements();
return;
}
}
else if (event->matches(QKeySequence::Cut))
{
if (hasSelectedElements())
{
std::string str = getSelectedString();
deleteSelectedElements();
QApplication::clipboard()->setText(QString::fromStdString(str));
return;
}
}
else if (event->matches(QKeySequence::Copy))
{
if (hasSelectedElements())
{
std::string str = getSelectedString();
QApplication::clipboard()->setText(QString::fromStdString(str));
return;
}
}
else if (event->matches(QKeySequence::Paste))
{
setEditText(text() + QApplication::clipboard()->text());
onTextEdited(text());
return;
}
QLineEdit::keyPressEvent(event);
}
void QtSmartSearchBox::keyReleaseEvent(QKeyEvent* event)
{
m_shiftKeyDown = event->modifiers() & Qt::ShiftModifier;
}
void QtSmartSearchBox::mouseMoveEvent(QMouseEvent* event)
{
QLineEdit::mouseMoveEvent(event);
if (!m_mousePressed)
{
return;
}
int lo = event->x() < m_mouseX ? event->x() : m_mouseX;
int hi = event->x() > m_mouseX ? event->x() : m_mouseX;
for (size_t i = 0; i < m_elements.size(); i++)
{
int widgetX = m_elements[i]->x() + m_elements[i]->width() / 2;
m_elements[i]->setChecked(lo < widgetX && widgetX < hi);
}
editTextToElement();
layoutElements();
}
void QtSmartSearchBox::mousePressEvent(QMouseEvent* event)
{
QLineEdit::mousePressEvent(event);
m_mousePressed = true;
m_mouseX = event->x();
}
void QtSmartSearchBox::mouseReleaseEvent(QMouseEvent* event)
{
QLineEdit::mouseReleaseEvent(event);
m_mousePressed = false;
if (abs(event->x() - m_mouseX) > 5)
{
return;
}
int minDist = event->x();
int pos = 0;
for (size_t i = 0; i < m_elements.size(); i++)
{
int dist = m_elements[i]->x() + m_elements[i]->width() - event->x();
if (abs(dist) < abs(minDist))
{
pos = i + 1;
minDist = dist;
}
}
bool hasSelected = hasSelectedElements();
selectAllElementsWith(false);
if (pos - m_cursorIndex != 0)
{
moveCursor(pos - m_cursorIndex);
}
else if (hasSelected)
{
layoutElements();
}
}
void QtSmartSearchBox::onTextEdited(const QString& text)
{
m_allowTextChange = true;
deleteSelectedElements();
bool tokensChanged = false;
std::string token;
std::deque<std::string> tokens = QueryTree::tokenizeQuery(text.toStdString());
while (tokens.size())
{
token = tokens.front();
tokens.pop_front();
if (tokens.size() || QueryTree::getTokenTypeName(token) != "none")
{
textToToken(token);
token.clear();
tokensChanged = true;
}
}
if (tokensChanged)
{
setEditText(QString::fromStdString(token));
updateElements();
}
else
{
layoutElements();
}
if (token.size() || m_elements.size())
{
requestAutoCompletions();
}
}
void QtSmartSearchBox::onTextChanged(const QString& text)
{
if (!m_allowTextChange)
{
setText(m_oldText);
}
else
{
m_oldText = text;
}
m_allowTextChange = false;
updatePlaceholder();
}
void QtSmartSearchBox::onAutocompletionHighlighted(const SearchMatch& match)
{
m_highlightedMatch = match;
}
void QtSmartSearchBox::onAutocompletionActivated(const SearchMatch& match)
{
searchMatchToToken(match);
if (match.fullName.size())
{
search();
}
}
void QtSmartSearchBox::onElementSelected(QtQueryElement* element)
{
if (!hasSelectedElements() && !m_shiftKeyDown)
{
editElement(element);
return;
}
size_t idx = 0;
bool checked = element->isChecked();
for (size_t i = 0; i < m_elements.size(); i++)
{
if (m_elements[i].get() == element)
{
idx = i;
break;
}
}
if (text().size())
{
if (m_cursorIndex <= idx)
{
idx++;
}
editTextToElement();
element = m_elements[idx].get();
element->setChecked(checked);
}
if (m_shiftKeyDown)
{
selectElementsTo(idx, checked);
}
else
{
selectAllElementsWith(false);
element->setChecked(true);
m_cursorIndex = idx + 1;
}
layoutElements();
}
void QtSmartSearchBox::moveCursor(int offset)
{
moveCursorTo(m_cursorIndex + offset);
}
void QtSmartSearchBox::moveCursorTo(int target)
{
if (target >= 0 && target <= static_cast<int>(m_elements.size()))
{
m_cursorIndex = target;
layoutElements();
hideAutoCompletions();
}
}
void QtSmartSearchBox::textToToken(std::string text)
{
if (!text.size())
{
return;
}
if (completer()->popup()->isVisible())
{
const SearchMatch* match = dynamic_cast<QtAutocompletionList*>(completer())->getSearchMatchAt(0);
if (match && utility::equalsCaseInsensitive(text, match->fullName))
{
text = match->encodeForQuery();
}
}
m_tokens.insert(m_tokens.begin() + m_cursorIndex, text);
m_cursorIndex++;
}
void QtSmartSearchBox::searchMatchToToken(const SearchMatch& match)
{
if (match.fullName.size())
{
m_oldText.clear();
clearLineEdit();
std::string name = match.encodeForQuery();
textToToken(name);
updateElements();
}
}
void QtSmartSearchBox::setEditText(const QString& text)
{
m_allowTextChange = true;
setText(text);
}
bool QtSmartSearchBox::editTextToElement()
{
if (text().size())
{
textToToken(text().toStdString());
clearLineEdit();
updateElements();
return true;
}
return false;
}
void QtSmartSearchBox::editElement(QtQueryElement* element)
{
for (int i = m_elements.size() - 1; i >= 0; i--)
{
if (m_elements[i].get() == element)
{
m_cursorIndex = i;
break;
}
}
std::string token = QueryTree::getTokenName(m_tokens[m_cursorIndex]);
m_tokens.erase(m_tokens.begin() + m_cursorIndex);
setEditText(QString::fromStdString(token));
updateElements();
requestAutoCompletions();
}
void QtSmartSearchBox::updateElements()
{
m_elements.clear();
for (const std::string& token : m_tokens)
{
std::string name = QueryTree::getTokenName(token);
name = utility::replace(name, "&", "&&");
std::shared_ptr<QtQueryElement> element = std::make_shared<QtQueryElement>(QString::fromStdString(name), this);
m_elements.push_back(element);
element->setObjectName(QString::fromStdString("search_element_" + QueryTree::getTokenTypeName(token)));
connect(element.get(), SIGNAL(wasChecked(QtQueryElement*)), this, SLOT(onElementSelected(QtQueryElement*)));
}
setStyleSheet(TextAccess::createFromFile("data/gui/search_view/search_element.css")->getText().c_str());
updatePlaceholder();
hideAutoCompletions();
layoutElements();
}
void QtSmartSearchBox::layoutElements()
{
ensurePolished();
bool hasSelected = hasSelectedElements();
QString cursorText = text();
cursorText.resize(cursorPosition());
int x = 5;
int editX = x;
int cursorX = fontMetrics().width(cursorText) + x;
std::vector<int> elementX;
int highlightBegin = 0;
int highlightEnd = 0;
for (size_t i = 0; i <= m_elements.size(); i++)
{
if (!hasSelected && i == m_cursorIndex)
{
editX = x - 5;
cursorX += editX;
if (i != m_elements.size())
{
cursorX += 15;
}
x += fontMetrics().width(text());
}
if (i < m_elements.size())
{
QtQueryElement* button = m_elements[i].get();
if (button->isChecked() && !highlightBegin)
{
highlightBegin = x - 2;
}
elementX.push_back(x);
x += button->minimumSizeHint().width() + 5;
if (button->isChecked())
{
highlightEnd = x - 3;
}
}
}
int offsetX = 0;
if (cursorX > width())
{
offsetX = width() - cursorX;
}
for (size_t i = 0; i < elementX.size(); i++)
{
QtQueryElement* button = m_elements[i].get();
QSize size = button->minimumSizeHint();
int y = (rect().height() - size.height()) / 2.0;
button->setGeometry(elementX[i] + offsetX, y, size.width(), size.height());
}
if (hasSelected)
{
setTextMargins(width() + 10, 0, 0, 0);
m_highlightRect->setGeometry(highlightBegin + offsetX, 0, highlightEnd - highlightBegin, height());
}
else
{
QMargins margins = textMargins();
setTextMargins(editX + offsetX, margins.top(), margins.right(), margins.bottom());
m_highlightRect->setGeometry(0, 0, 0, 0);
}
}
bool QtSmartSearchBox::hasSelectedElements() const
{
for (const std::shared_ptr<QtQueryElement> element : m_elements)
{
if (element->isChecked())
{
return true;
}
}
return false;
}
std::string QtSmartSearchBox::getSelectedString() const
{
std::string str;
for (size_t i = 0; i < m_elements.size(); i++)
{
if (m_elements[i]->isChecked())
{
str += m_tokens[i];
}
}
return str;
}
void QtSmartSearchBox::selectAllElementsWith(bool selected)
{
for (const std::shared_ptr<QtQueryElement> element : m_elements)
{
element->setChecked(selected);
}
}
void QtSmartSearchBox::selectElementsTo(size_t idx, bool selected)
{
size_t low = idx < m_cursorIndex ? idx : m_cursorIndex;
size_t hi = idx > m_cursorIndex ? idx + 1 : m_cursorIndex;
while (low < hi)
{
m_elements[low]->setChecked(selected);
low++;
}
if (!selected)
{
m_elements[idx]->setChecked(true);
}
if (idx < m_cursorIndex)
{
m_cursorIndex = idx;
}
else
{
m_cursorIndex = idx + 1;
}
}
void QtSmartSearchBox::deleteSelectedElements()
{
if (!hasSelectedElements())
{
return;
}
for (size_t i = m_elements.size(); i > 0; i--)
{
if (m_elements[i - 1]->isChecked())
{
m_tokens.erase(m_tokens.begin() + (i - 1));
if ((i - 1) < m_cursorIndex)
{
m_cursorIndex--;
}
}
}
updateElements();
}
void QtSmartSearchBox::updatePlaceholder()
{
if (!text().size() && !m_elements.size())
{
setPlaceholderText("Please enter your search string.");
}
else
{
setPlaceholderText("");
}
}
void QtSmartSearchBox::clearLineEdit()
{
setEditText("");
hideAutoCompletions();
}
void QtSmartSearchBox::requestAutoCompletions() const
{
std::string query;
for (size_t i = 0; i < m_tokens.size() && i < m_cursorIndex; i++)
{
query += m_tokens[i];
}
MessageSearchAutocomplete(query, text().toStdString()).dispatch();
}
void QtSmartSearchBox::hideAutoCompletions()
{
completer()->popup()->hide();
}