diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp
index 1d7dee006..befeccd6e 100644
--- a/src/gui/EntryPreviewWidget.cpp
+++ b/src/gui/EntryPreviewWidget.cpp
@@ -402,7 +402,7 @@ void EntryPreviewWidget::updateEntryGeneralTab()
const TimeInfo entryTime = m_currentEntry->timeInfo();
const QString expires = entryTime.expires() ? Clock::toString(entryTime.expiryTime().toLocalTime()) : tr("Never");
m_ui->entryExpirationLabel->setText(expires);
- m_ui->entryTagsList->tags(m_currentEntry->tagList());
+ m_ui->entryTagsList->setTags(m_currentEntry->tagList());
m_ui->entryTagsList->setReadOnly(true);
}
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 9af9aa601..ed2fb176a 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -948,8 +948,8 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
m_mainUi->usernameComboBox->lineEdit()->setReadOnly(m_history);
m_mainUi->urlEdit->setReadOnly(m_history);
m_mainUi->passwordEdit->setReadOnly(m_history);
- m_mainUi->tagsList->tags(entry->tagList());
- m_mainUi->tagsList->completion(m_db->tagList());
+ m_mainUi->tagsList->setTags(entry->tagList());
+ m_mainUi->tagsList->setCompletion(m_db->tagList());
m_mainUi->expireCheck->setEnabled(!m_history);
m_mainUi->expireDatePicker->setReadOnly(m_history);
m_mainUi->revealNotesButton->setIcon(icons()->onOffIcon("password-show", false));
diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui
index 6fe6637e7..087406432 100644
--- a/src/gui/entry/EditEntryWidgetMain.ui
+++ b/src/gui/entry/EditEntryWidgetMain.ui
@@ -143,13 +143,25 @@
-
-
+
+
+
+ 16777215
+ 70
+
+
Qt::StrongFocus
Tags list
+
+ QFrame::Panel
+
+
+ QAbstractScrollArea::AdjustToContents
+
-
@@ -345,7 +357,7 @@
TagsEdit
- QWidget
+ QScrollArea
1
diff --git a/src/gui/tag/TagsEdit.cpp b/src/gui/tag/TagsEdit.cpp
index 6bd0d39df..8ba6ed4ce 100644
--- a/src/gui/tag/TagsEdit.cpp
+++ b/src/gui/tag/TagsEdit.cpp
@@ -23,12 +23,13 @@
*/
#include "TagsEdit.h"
-#include "gui/MainWindow.h"
+
#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -37,179 +38,227 @@
#include
#include
-#include
-
namespace
{
+ constexpr int TAG_V_SPACING = 4;
+ constexpr int TAG_H_SPACING = 4;
- constexpr int tag_v_spacing = 2;
- constexpr int tag_h_spacing = 3;
+ constexpr QMargins TAG_INNER(5, 3, 4, 3);
- constexpr QMargins tag_inner(5, 3, 4, 3);
+ constexpr int TAG_CROSS_WIDTH = 5;
+ constexpr float TAG_CROSS_RADIUS = TAG_CROSS_WIDTH / 2;
+ constexpr int TAG_CROSS_PADDING = 5;
- constexpr int tag_cross_width = 5;
- constexpr float tag_cross_radius = tag_cross_width / 2;
- constexpr int tag_cross_padding = 5;
-
- struct Tag
+ class Tag
{
+ public:
+ Tag() = default;
+ Tag(const QString& text) : text(text.trimmed()), rect(), row() {}
+
bool isEmpty() const noexcept
{
return text.isEmpty();
}
QString text;
- QRect rect;
- size_t row;
+ public:
+ // Render state
+ mutable QRect rect;
+ mutable size_t row;
};
- /// Non empty string filtering iterator
- template struct EmptySkipIterator
- {
- EmptySkipIterator() = default;
-
- // skip until `end`
- explicit EmptySkipIterator(It it, It end)
- : it(it)
- , end(end)
- {
- while (this->it != end && this->it->isEmpty()) {
- ++this->it;
- }
- begin = it;
- }
-
- explicit EmptySkipIterator(It it)
- : it(it)
- , end{}
- {
- }
-
- using difference_type = typename std::iterator_traits::difference_type;
- using value_type = typename std::iterator_traits::value_type;
- using pointer = typename std::iterator_traits::pointer;
- using reference = typename std::iterator_traits::reference;
- using iterator_category = std::output_iterator_tag;
-
- EmptySkipIterator& operator++()
- {
- assert(it != end);
- while (++it != end && it->isEmpty())
- ;
- return *this;
- }
-
- decltype(auto) operator*()
- {
- return *it;
- }
-
- pointer operator->()
- {
- return &(*it);
- }
-
- bool operator!=(EmptySkipIterator const& rhs) const
- {
- return it != rhs.it;
- }
-
- bool operator==(EmptySkipIterator const& rhs) const
- {
- return it == rhs.it;
- }
-
- private:
- It begin;
- It it;
- It end;
- };
-
- template EmptySkipIterator(It, It) -> EmptySkipIterator;
-
} // namespace
+class TagManager {
+public:
+ using iterator = QLinkedList::iterator;
+ using const_iterator = QLinkedList::const_iterator;
+
+ TagManager() : tags{Tag()}, editing_index(tags.begin()) {}
+ template TagManager(InputIterator begin, InputIterator end) {
+ QSet unique_tags;
+ for (auto it = begin; it != end; ++it) {
+ Tag new_tag(*it);
+ if (unique_tags.contains(new_tag.text)) {
+ continue;
+ }
+ unique_tags.insert(new_tag.text);
+ tags.push_back(new_tag);
+ }
+
+ if (tags.isEmpty()) {
+ tags.push_back(Tag());
+ }
+ editing_index = tags.begin();
+ }
+
+ iterator begin() { return tags.begin(); }
+ iterator end() { return tags.end(); }
+ const_iterator begin() const { return tags.begin(); }
+ const_iterator end() const { return tags.end(); }
+ const_iterator cbegin() const { return tags.cbegin(); }
+ const_iterator cend() const { return tags.cend(); }
+
+ const Tag& back() const {
+ return tags.back();
+ }
+
+ const Tag& front() const {
+ return tags.front();
+ }
+
+ iterator editingIndex() { return editing_index; }
+
+ const_iterator editingIndex() const { return editing_index; }
+
+ bool isCurrentTextEmpty() const {
+ return editing_index->isEmpty();
+ }
+
+ void setEditingIndex(const iterator& it) {
+ if (editing_index == it) {
+ return;
+ }
+ // Ensure Invariant-1. If the previously edited tag is empty, remove it.
+ auto occurrencesOfCurrentText =
+ std::count_if(tags.cbegin(), tags.cend(), [this](const auto& tag) { return tag.text == editing_index->text; });
+ if (isCurrentTextEmpty() || occurrencesOfCurrentText > 1) {
+ erase(editing_index);
+ }
+ editing_index = it;
+ }
+
+ iterator insert(const iterator& it, const Tag& tag) {
+ return tags.insert(it, tag);
+ }
+
+ iterator erase(const iterator& it) {
+ bool current_index_needs_update = it == editing_index;
+
+ auto next = tags.erase(it);
+ if (next == tags.end()) {
+ next = std::prev(next);
+ }
+
+ if (current_index_needs_update) {
+ editing_index = next;
+ }
+
+ return next;
+
+ }
+
+ bool isEmpty() const { return tags.isEmpty(); }
+ int size() const { return tags.size(); }
+
+private:
+ QLinkedList tags;
+ // TODO Rename
+ iterator editing_index;
+};
+
// Invariant-1 ensures no empty tags apart from currently being edited.
// Default-state is one empty tag which is currently editing.
struct TagsEdit::Impl
{
+ using iterator = QLinkedList::iterator;
+ using const_iterator = QLinkedList::const_iterator;
+
explicit Impl(TagsEdit* ifce)
: ifce(ifce)
- , tags{Tag()}
- , editing_index(0)
, cursor(0)
- , blink_timer(0)
- , blink_status(true)
, select_start(0)
, select_size(0)
, cross_deleter(true)
- , completer(std::make_unique())
{
}
- inline QRectF crossRect(QRectF const& r) const
+ iterator begin()
{
- QRectF cross(QPointF{0, 0}, QSizeF{tag_cross_width + tag_cross_padding * 2, r.top() - r.bottom()});
- cross.moveCenter(QPointF(r.right() - tag_cross_radius - tag_cross_padding, r.center().y()));
+ return tags.begin();
+ }
+
+ iterator end()
+ {
+ return tags.end();
+ }
+
+ const_iterator begin() const
+ {
+ return tags.begin();
+ }
+
+ const_iterator end() const
+ {
+ return tags.end();
+ }
+
+ QRectF crossRect(const QRectF& r) const
+ {
+ QRectF cross(QPointF{0, 0}, QSizeF{TAG_CROSS_WIDTH + TAG_CROSS_PADDING * 2, r.top() - r.bottom()});
+ cross.moveCenter(QPointF(r.right() - TAG_CROSS_RADIUS - TAG_CROSS_PADDING, r.center().y()));
return cross;
}
- bool inCrossArea(int tag_index, QPoint point) const
+ bool isBeingEdited(const const_iterator& it) const
+ {
+ return it == tags.editingIndex();
+ }
+
+ bool inCrossArea(const const_iterator& it, const QPoint& point) const
{
return cross_deleter
- ? crossRect(tags[tag_index].rect)
- .adjusted(-tag_cross_radius, 0, 0, 0)
+ ? crossRect(it->rect)
+ .adjusted(-TAG_CROSS_RADIUS, 0, 0, 0)
.translated(-ifce->horizontalScrollBar()->value(), -ifce->verticalScrollBar()->value())
.contains(point)
- && (!cursorVisible() || tag_index != editing_index)
+ && (!cursorVisible() || !isBeingEdited(it))
: false;
}
- template void drawTags(QPainter& p, std::pair range) const
+ void drawTag(QPainter& p, const Tag& tag) const
{
- for (auto it = range.first; it != range.second; ++it) {
- QRect const& i_r =
- it->rect.translated(-ifce->horizontalScrollBar()->value(), -ifce->verticalScrollBar()->value());
- auto const text_pos =
- i_r.topLeft()
- + QPointF(tag_inner.left(),
- ifce->fontMetrics().ascent() + ((i_r.height() - ifce->fontMetrics().height()) / 2));
+ QRect const& i_r =
+ tag.rect.translated(-ifce->horizontalScrollBar()->value(), -ifce->verticalScrollBar()->value());
+ const auto text_pos =
+ i_r.topLeft()
+ + QPointF(TAG_INNER.left(),
+ ifce->fontMetrics().ascent() + ((i_r.height() - ifce->fontMetrics().height()) / 2));
- // draw tag rect
- auto palette = getMainWindow()->palette();
- QPainterPath path;
- auto cornerRadius = 4;
- path.addRoundedRect(i_r, cornerRadius, cornerRadius);
- p.fillPath(path, palette.brush(QPalette::ColorGroup::Inactive, QPalette::ColorRole::Highlight));
+ // draw tag rect
+ auto palette = ifce->palette();
+ QPainterPath path;
+ auto cornerRadius = 4;
+ path.addRoundedRect(i_r, cornerRadius, cornerRadius);
+ p.fillPath(path, palette.brush(QPalette::ColorGroup::Inactive, QPalette::ColorRole::Highlight));
- // draw text
- p.drawText(text_pos, it->text);
+ // draw text
+ p.drawText(text_pos, tag.text);
- if (cross_deleter) {
- // calc cross rect
- auto const i_cross_r = crossRect(i_r);
+ if (cross_deleter) {
+ // calc cross rect
+ const auto i_cross_r = crossRect(i_r);
- QPainterPath crossRectBg1, crossRectBg2;
- crossRectBg1.addRoundedRect(i_cross_r, cornerRadius, cornerRadius);
- // cover left rounded corners
- crossRectBg2.addRect(
- i_cross_r.left(), i_cross_r.bottom(), tag_cross_radius, i_cross_r.top() - i_cross_r.bottom());
- p.fillPath(crossRectBg1, palette.highlight());
- p.fillPath(crossRectBg2, palette.highlight());
+ QPainterPath crossRectBg1, crossRectBg2;
+ crossRectBg1.addRoundedRect(i_cross_r, cornerRadius, cornerRadius);
+ // cover left rounded corners
+ crossRectBg2.addRect(
+ i_cross_r.left(), i_cross_r.bottom(), TAG_CROSS_RADIUS, i_cross_r.top() - i_cross_r.bottom());
+ p.fillPath(crossRectBg1, palette.highlight());
+ p.fillPath(crossRectBg2, palette.highlight());
- QPen pen = p.pen();
- pen.setWidth(2);
- pen.setBrush(palette.highlightedText());
+ QPen pen = p.pen();
+ pen.setWidth(2);
+ pen.setBrush(palette.highlightedText());
- p.save();
- p.setPen(pen);
- p.setRenderHint(QPainter::Antialiasing);
- p.drawLine(QLineF(i_cross_r.center() - QPointF(tag_cross_radius, tag_cross_radius),
- i_cross_r.center() + QPointF(tag_cross_radius, tag_cross_radius)));
- p.drawLine(QLineF(i_cross_r.center() - QPointF(-tag_cross_radius, tag_cross_radius),
- i_cross_r.center() + QPointF(-tag_cross_radius, tag_cross_radius)));
- p.restore();
- }
+ p.save();
+ p.setPen(pen);
+ p.setRenderHint(QPainter::Antialiasing);
+ p.drawLine(QLineF(i_cross_r.center() - QPointF(TAG_CROSS_RADIUS, TAG_CROSS_RADIUS),
+ i_cross_r.center() + QPointF(TAG_CROSS_RADIUS, TAG_CROSS_RADIUS)));
+ p.drawLine(QLineF(i_cross_r.center() - QPointF(-TAG_CROSS_RADIUS, TAG_CROSS_RADIUS),
+ i_cross_r.center() + QPointF(-TAG_CROSS_RADIUS, TAG_CROSS_RADIUS)));
+ p.restore();
}
}
@@ -218,108 +267,52 @@ struct TagsEdit::Impl
return ifce->viewport()->contentsRect();
}
- QRect calcRects(QList& tags) const
+ QRect updateTagRenderStates()
{
- return calcRects(tags, contentsRect());
+ return updateTagRenderStates(contentsRect());
}
- QRect calcRects(QList& tags, QRect r) const
+ QRect updateTagRenderStates(QRect r)
{
size_t row = 0;
auto lt = r.topLeft();
QFontMetrics fm = ifce->fontMetrics();
- auto const b = std::begin(tags);
- auto const e = std::end(tags);
- if (cursorVisible()) {
- auto const m = b + static_cast(editing_index);
- calcRects(lt, row, r, fm, std::pair(b, m));
- calcEditorRect(lt, row, r, fm, m);
- calcRects(lt, row, r, fm, std::pair(m + 1, e));
- } else {
- calcRects(lt, row, r, fm, std::pair(b, e));
+ for(auto it = std::begin(tags); it != std::end(tags); ++it) {
+ updateTagRenderState(lt, row, r, fm, *it, it == tags.editingIndex() && cursorVisible());
}
- r.setBottom(lt.y() + fm.height() + fm.leading() + tag_inner.top() + tag_inner.bottom() - 1);
+ r.setBottom(lt.y() + fm.height() + fm.leading() + TAG_INNER.top() + TAG_INNER.bottom() - 1);
return r;
}
- template
- void calcRects(QPoint& lt, size_t& row, QRect r, QFontMetrics const& fm, std::pair range) const
+ void updateTagRenderState(QPoint& lt, size_t& row, QRect r, QFontMetrics const& fm, const Tag& tag, bool isBeingEdited) const
{
- for (auto it = range.first; it != range.second; ++it) {
- // calc text rect
- const auto text_w = fm.horizontalAdvance(it->text);
- auto const text_h = fm.height() + fm.leading();
- auto const w = cross_deleter
- ? tag_inner.left() + tag_inner.right() + tag_cross_padding * 2 + tag_cross_width
- : tag_inner.left() + tag_inner.right();
- auto const h = tag_inner.top() + tag_inner.bottom();
- QRect i_r(lt, QSize(text_w + w, text_h + h));
-
- // line wrapping
- if (r.right() < i_r.right() && // doesn't fit in current line
- i_r.left() != r.left() // doesn't occupy entire line already
- ) {
- i_r.moveTo(r.left(), i_r.bottom() + tag_v_spacing);
- ++row;
- lt = i_r.topLeft();
- }
-
- it->rect = i_r;
- it->row = row;
- lt.setX(i_r.right() + tag_h_spacing);
- }
- }
-
- template void calcEditorRect(QPoint& lt, size_t& row, QRect r, QFontMetrics const& fm, It it) const
- {
- auto const text_w = fm.horizontalAdvance(text_layout.text());
- auto const text_h = fm.height() + fm.leading();
- auto const w = tag_inner.left() + tag_inner.right();
- auto const h = tag_inner.top() + tag_inner.bottom();
+ // calc text rect
+ const auto text_w = fm.horizontalAdvance(tag.text);
+ const auto text_h = fm.height() + fm.leading();
+ const auto w = (cross_deleter && !isBeingEdited)
+ ? TAG_INNER.left() + TAG_INNER.right() + TAG_CROSS_PADDING * 2 + TAG_CROSS_WIDTH
+ : TAG_INNER.left() + TAG_INNER.right();
+ const auto h = TAG_INNER.top() + TAG_INNER.bottom();
QRect i_r(lt, QSize(text_w + w, text_h + h));
// line wrapping
- if (r.right() < i_r.right() && // doesn't fit in current line
- i_r.left() != r.left() // doesn't occupy entire line already
- ) {
- i_r.moveTo(r.left(), i_r.bottom() + tag_v_spacing);
+ // doesn't fit in current line && doesn't occupy entire line already
+ if (r.right() < i_r.right() && i_r.left() != r.left()) {
+ i_r.moveTo(r.left(), i_r.bottom() + TAG_V_SPACING);
++row;
lt = i_r.topLeft();
}
- it->rect = i_r;
- it->row = row;
- lt.setX(i_r.right() + tag_h_spacing);
- }
-
- void setCursorVisible(bool visible)
- {
- if (blink_timer) {
- ifce->killTimer(blink_timer);
- blink_timer = 0;
- blink_status = true;
- }
-
- if (visible) {
- int flashTime = QGuiApplication::styleHints()->cursorFlashTime();
- if (flashTime >= 2) {
- blink_timer = ifce->startTimer(flashTime / 2);
- }
- } else {
- blink_status = false;
- }
+ tag.rect = i_r;
+ tag.row = row;
+ lt.setX(i_r.right() + TAG_H_SPACING);
}
bool cursorVisible() const
{
- return blink_timer;
- }
-
- void updateCursorBlinking()
- {
- setCursorVisible(cursorVisible());
+ return ifce->cursorVisible();
}
void updateDisplayText()
@@ -331,35 +324,39 @@ struct TagsEdit::Impl
text_layout.endLayout();
}
+ bool isEmptyTag(const iterator& it) {
+ return it->text.trimmed().isEmpty();
+ }
+
+ bool isCurrentTagEmpty() {
+ return isEmptyTag(tags.editingIndex());
+ }
+
/// Makes the tag at `i` currently editing, and ensures Invariant-1`.
- void setEditingIndex(int i)
+ void setEditingIndex(const iterator& it)
{
- assert(i < tags.size());
- auto occurrencesOfCurrentText =
- std::count_if(tags.cbegin(), tags.cend(), [this](const auto& tag) { return tag.text == currentText(); });
- if (currentText().isEmpty() || occurrencesOfCurrentText > 1) {
- tags.erase(std::next(tags.begin(), std::ptrdiff_t(editing_index)));
- if (editing_index <= i) { // Do we shift positions after `i`?
- --i;
- }
- }
- editing_index = i;
+ tags.setEditingIndex(it);
+ }
+
+ void insertText(const QString& text) {
+ currentText().insert(cursor, text);
+ moveCursor(cursor + text.size(), false);
}
void calcRectsAndUpdateScrollRanges()
{
- auto const row = tags.back().row;
- auto const max_width = std::max_element(std::begin(tags), std::end(tags), [](auto const& x, auto const& y) {
+ const auto row = tags.back().row;
+ const auto max_width = std::max_element(std::begin(tags), std::end(tags), [](const auto& x, const auto& y) {
return x.rect.width() < y.rect.width();
})->rect.width();
- calcRects(tags);
+ updateTagRenderStates();
if (row != tags.back().row) {
updateVScrollRange();
}
- auto const new_max_width = std::max_element(std::begin(tags), std::end(tags), [](auto const& x, auto const& y) {
+ const auto new_max_width = std::max_element(std::begin(tags), std::end(tags), [](const auto& x, const auto& y) {
return x.rect.width() < y.rect.width();
})->rect.width();
@@ -368,8 +365,9 @@ struct TagsEdit::Impl
}
}
- void currentText(QString const& text)
+ void setCurrentText(const QString& text)
{
+ Q_ASSERT(tags.editingIndex() != tags.end());
currentText() = text;
moveCursor(currentText().length(), false);
updateDisplayText();
@@ -377,42 +375,28 @@ struct TagsEdit::Impl
ifce->viewport()->update();
}
- QString const& currentText() const
+ QString currentText() const
{
- return tags[editing_index].text;
+ Q_ASSERT(tags.editingIndex() != tags.end());
+ return tags.editingIndex()->text;
}
- QString& currentText()
+ QRect currentRect() const
{
- return tags[editing_index].text;
- }
-
- QRect const& currentRect() const
- {
- return tags[editing_index].rect;
+ Q_ASSERT(tags.editingIndex() != tags.end());
+ return tags.editingIndex()->rect;
}
// Inserts a new tag at `i`, makes the tag currently editing,
// and ensures Invariant-1.
- void editNewTag(int i)
+ void editNewTag(const iterator& i)
{
currentText() = currentText().trimmed();
- tags.insert(std::next(std::begin(tags), static_cast(i)), Tag());
- if (editing_index >= i) {
- ++editing_index;
- }
- setEditingIndex(i);
+ auto inserted_at = tags.insert(i, Tag());
+ setEditingIndex(inserted_at);
moveCursor(0, false);
}
- void setupCompleter()
- {
- completer->setWidget(ifce);
- connect(completer.get(),
- static_cast(&QCompleter::activated),
- [this](QString const& text) { currentText(text); });
- }
-
QVector formatting() const
{
if (select_size == 0) {
@@ -476,6 +460,15 @@ struct TagsEdit::Impl
cursor = pos;
}
+ bool finishTag() {
+ // Make existing text into a tag
+ if (!isCurrentTagEmpty()) {
+ editNewTag(std::next(tags.editingIndex()));
+ return true;
+ }
+ return false;
+ }
+
qreal cursorToX()
{
return text_layout.lineAt(0).cursorToX(cursor);
@@ -483,34 +476,88 @@ struct TagsEdit::Impl
void editPreviousTag()
{
- if (editing_index > 0) {
- setEditingIndex(editing_index - 1);
+ if (tags.editingIndex() != begin()) {
+ setEditingIndex(std::prev(tags.editingIndex()));
moveCursor(currentText().size(), false);
}
}
- void editNextTag()
+ template void setTags(InputIterator begin, InputIterator end) {
+ cursor = 0;
+ select_start = 0;
+ select_size = 0;
+
+ tags = TagManager(begin, end);
+ }
+
+ void editNextTag(bool add_new = false)
{
- if (editing_index < tags.size() - 1) {
- setEditingIndex(editing_index + 1);
+ if (tags.editingIndex() != std::prev(end())) {
+ setEditingIndex(std::next(tags.editingIndex()));
+ moveCursor(0, false);
+ } else if (add_new) {
+ editNewTag(std::next(tags.editingIndex()));
+ }
+ }
+
+ void previousCursorPosition() {
+ if (cursor == 0) {
+ editPreviousTag();
+ } else {
+ moveCursor(text_layout.previousCursorPosition(cursor), false);
+ }
+ }
+
+ void nextCursorPosition() {
+ if (cursor == currentText().size()) {
+ editNextTag();
+ } else {
+ moveCursor(text_layout.nextCursorPosition(cursor), false);
+ }
+ }
+
+ void jumpToFront() {
+ if (cursor == 0 && !isBeingEdited(tags.begin())) {
+ editTag(tags.begin());
+ } else {
moveCursor(0, false);
}
}
- void editTag(int i)
+ void jumpToBack() {
+ if (cursor == currentText().size()) {
+ editTag(std::prev(tags.end()));
+ } else {
+ moveCursor(currentText().size(), false);
+ }
+ }
+
+ void selectNext() {
+ moveCursor(text_layout.nextCursorPosition(cursor), true);
+ }
+
+ void selectPrev() {
+ moveCursor(text_layout.previousCursorPosition(cursor), true);
+ }
+
+ void editTag(const iterator& i)
{
- assert(i >= 0 && i < tags.size());
setEditingIndex(i);
moveCursor(currentText().size(), false);
}
+ void removeTag(const iterator& i)
+ {
+ tags.erase(i);
+ }
+
void updateVScrollRange()
{
auto fm = ifce->fontMetrics();
- auto const row_h = fm.height() + fm.leading() + tag_inner.top() + tag_inner.bottom() + tag_v_spacing;
+ const auto row_h = fm.height() + fm.leading() + TAG_INNER.top() + TAG_INNER.bottom() + TAG_V_SPACING;
ifce->verticalScrollBar()->setPageStep(row_h);
- auto const h = tags.back().rect.bottom() - tags.front().rect.top() + 1;
- auto const contents_rect = contentsRect();
+ const auto h = tags.back().rect.bottom() - tags.front().rect.top() + 1;
+ const auto contents_rect = contentsRect();
if (h > contents_rect.height()) {
ifce->verticalScrollBar()->setRange(0, h - contents_rect.height());
} else {
@@ -520,7 +567,7 @@ struct TagsEdit::Impl
void updateHScrollRange()
{
- auto const max_width = std::max_element(std::begin(tags), std::end(tags), [](auto const& x, auto const& y) {
+ const auto max_width = std::max_element(std::begin(tags), std::end(tags), [](const auto& x, const auto& y) {
return x.rect.width() < y.rect.width();
})->rect.width();
updateHScrollRange(max_width);
@@ -528,7 +575,8 @@ struct TagsEdit::Impl
void updateHScrollRange(int width)
{
- auto const contents_rect_width = contentsRect().width();
+ // TODO Transform to getHScrollRange. Handle in iface
+ const auto contents_rect_width = contentsRect().width();
if (width > contents_rect_width) {
ifce->horizontalScrollBar()->setRange(0, width - contents_rect_width);
} else {
@@ -539,11 +587,11 @@ struct TagsEdit::Impl
void ensureCursorIsVisibleV()
{
auto fm = ifce->fontMetrics();
- auto const row_h = fm.height() + fm.leading() + tag_inner.top() + tag_inner.bottom();
- auto const vscroll = ifce->verticalScrollBar()->value();
- auto const cursor_top = currentRect().topLeft() + QPoint(qRound(cursorToX()), 0);
- auto const cursor_bottom = cursor_top + QPoint(0, row_h - 1);
- auto const contents_rect = contentsRect().translated(0, vscroll);
+ const auto row_h = fm.height() + fm.leading() + TAG_INNER.top() + TAG_INNER.bottom();
+ const auto vscroll = ifce->verticalScrollBar()->value();
+ const auto cursor_top = currentRect().topLeft() + QPoint(qRound(cursorToX()), 0);
+ const auto cursor_bottom = cursor_top + QPoint(0, row_h - 1);
+ const auto contents_rect = contentsRect().translated(0, vscroll);
if (contents_rect.bottom() < cursor_bottom.y()) {
ifce->verticalScrollBar()->setValue(cursor_bottom.y() - row_h);
} else if (cursor_top.y() < contents_rect.top()) {
@@ -553,9 +601,9 @@ struct TagsEdit::Impl
void ensureCursorIsVisibleH()
{
- auto const hscroll = ifce->horizontalScrollBar()->value();
- auto const contents_rect = contentsRect().translated(hscroll, 0);
- auto const cursor_x = (currentRect() - tag_inner).left() + qRound(cursorToX());
+ const auto hscroll = ifce->horizontalScrollBar()->value();
+ const auto contents_rect = contentsRect().translated(hscroll, 0);
+ const auto cursor_x = (currentRect() - TAG_INNER).left() + qRound(cursorToX());
if (contents_rect.right() < cursor_x) {
ifce->horizontalScrollBar()->setValue(cursor_x - contents_rect.width());
} else if (cursor_x < contents_rect.left()) {
@@ -563,24 +611,57 @@ struct TagsEdit::Impl
}
}
+private:
TagsEdit* const ifce;
- QList tags;
- int editing_index;
+ TagManager tags;
int cursor;
- int blink_timer;
- bool blink_status;
- QTextLayout text_layout;
int select_start;
int select_size;
bool cross_deleter;
- std::unique_ptr completer;
int hscroll{0};
+ QTextLayout text_layout;
+
+public:
+ void setReadOnly(bool readOnly) {
+ cross_deleter = !readOnly;
+ }
+
+ QTextLine lineAt(int i) const
+ {
+ return text_layout.lineAt(i);
+ }
+
+ void paint(QPainter& p, QPointF scrollOffsets, int fontHeight, bool drawCursor)
+ { // clip
+ const auto rect = contentsRect();
+ p.setClipRect(rect);
+
+ for (auto it = std::begin(tags); it != std::end(tags); ++it) {
+ if (cursorVisible() && isBeingEdited(it)) {
+ const auto r = currentRect();
+ const auto txt_p = r.topLeft() + QPoint(TAG_INNER.left(), ((r.height() - fontHeight) / 2));
+
+ // Nothing to draw. Don't draw anything to avoid adding text margins.
+ if (!it->isEmpty()) {
+ // draw not terminated tag
+ text_layout.draw(&p, txt_p - scrollOffsets, formatting());
+ }
+
+ // draw cursor
+ if (drawCursor) {
+ text_layout.drawCursor(&p, txt_p - scrollOffsets, cursor);
+ }
+ } else if(!it->isEmpty()) {
+ drawTag(p, *it);
+ }
+ }
+ }
};
TagsEdit::TagsEdit(QWidget* parent)
: QAbstractScrollArea(parent)
- , impl(std::make_unique(this))
- , m_readOnly(false)
+ , impl(new Impl(this))
+ , completer(new QCompleter)
{
QSizePolicy size_policy(QSizePolicy::Ignored, QSizePolicy::Preferred);
size_policy.setHeightForWidth(true);
@@ -591,11 +672,11 @@ TagsEdit::TagsEdit(QWidget* parent)
setAttribute(Qt::WA_InputMethodEnabled, true);
setMouseTracking(true);
- impl->setupCompleter();
- impl->setCursorVisible(hasFocus());
+ setupCompleter();
+ setCursorVisible(hasFocus());
impl->updateDisplayText();
- viewport()->setContentsMargins(1, 1, 1, 1);
+ viewport()->setContentsMargins(TAG_H_SPACING, TAG_V_SPACING, TAG_H_SPACING, TAG_V_SPACING);
}
TagsEdit::~TagsEdit() = default;
@@ -608,86 +689,88 @@ void TagsEdit::setReadOnly(bool readOnly)
setCursor(Qt::ArrowCursor);
setAttribute(Qt::WA_InputMethodEnabled, false);
setFrameShape(QFrame::NoFrame);
- impl->cross_deleter = false;
} else {
setFocusPolicy(Qt::StrongFocus);
setCursor(Qt::IBeamCursor);
setAttribute(Qt::WA_InputMethodEnabled, true);
- impl->cross_deleter = true;
}
+ impl->setReadOnly(m_readOnly);
}
void TagsEdit::resizeEvent(QResizeEvent*)
{
- impl->calcRects(impl->tags);
+ impl->updateTagRenderStates();
impl->updateVScrollRange();
impl->updateHScrollRange();
}
void TagsEdit::focusInEvent(QFocusEvent*)
{
- impl->setCursorVisible(true);
+ setCursorVisible(true);
impl->updateDisplayText();
- impl->calcRects(impl->tags);
- impl->completer->complete();
+ impl->updateTagRenderStates();
+ completer->complete();
viewport()->update();
}
void TagsEdit::focusOutEvent(QFocusEvent*)
{
- impl->setCursorVisible(false);
+ setCursorVisible(false);
impl->updateDisplayText();
- impl->calcRects(impl->tags);
- impl->completer->popup()->hide();
+ impl->updateTagRenderStates();
+ completer->popup()->hide();
viewport()->update();
+ // TODO This fixes a bug where an empty tag was shown
+ impl->finishTag();
}
void TagsEdit::hideEvent(QHideEvent* event)
{
Q_UNUSED(event)
- impl->completer->popup()->hide();
+ completer->popup()->hide();
+}
+
+void TagsEdit::setCursorVisible(bool visible)
+{
+ if (blink_timer) {
+ killTimer(blink_timer);
+ blink_timer = 0;
+ blink_status = true;
+ }
+
+ if (visible) {
+ int flashTime = QGuiApplication::styleHints()->cursorFlashTime();
+ if (flashTime >= 2) {
+ blink_timer = startTimer(flashTime / 2);
+ }
+ } else {
+ blink_status = false;
+ }
+}
+
+bool TagsEdit::cursorVisible() const
+{
+ return blink_timer != 0;
+}
+
+void TagsEdit::updateCursorBlinking()
+{
+ setCursorVisible(cursorVisible());
}
void TagsEdit::paintEvent(QPaintEvent*)
{
QPainter p(viewport());
+ QPointF scrollOffsets = QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value());
+ const auto fontHeight = fontMetrics().height();
- // clip
- auto const rect = impl->contentsRect();
- p.setClipRect(rect);
- if (impl->cursorVisible()) {
- // not terminated tag pos
- auto const& r = impl->currentRect();
- auto const& txt_p = r.topLeft() + QPointF(tag_inner.left(), ((r.height() - fontMetrics().height()) / 2));
-
- // tags
- impl->drawTags(
- p, std::pair(impl->tags.cbegin(), std::next(impl->tags.cbegin(), std::ptrdiff_t(impl->editing_index))));
-
- // draw not terminated tag
- auto const formatting = impl->formatting();
- impl->text_layout.draw(
- &p, txt_p - QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value()), formatting);
-
- // draw cursor
- if (impl->blink_status) {
- impl->text_layout.drawCursor(
- &p, txt_p - QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value()), impl->cursor);
- }
-
- // tags
- impl->drawTags(
- p, std::pair(std::next(impl->tags.cbegin(), std::ptrdiff_t(impl->editing_index + 1)), impl->tags.cend()));
- } else {
- impl->drawTags(
- p, std::pair(EmptySkipIterator(impl->tags.begin(), impl->tags.end()), EmptySkipIterator(impl->tags.end())));
- }
+ impl->paint(p, scrollOffsets, fontHeight, blink_status);
}
void TagsEdit::timerEvent(QTimerEvent* event)
{
- if (event->timerId() == impl->blink_timer) {
- impl->blink_status = !impl->blink_status;
+ if (event->timerId() == blink_timer) {
+ blink_status = !blink_status;
viewport()->update();
}
}
@@ -695,41 +778,36 @@ void TagsEdit::timerEvent(QTimerEvent* event)
void TagsEdit::mousePressEvent(QMouseEvent* event)
{
bool found = false;
- for (int i = 0; i < impl->tags.size(); ++i) {
- if (impl->inCrossArea(i, event->pos())) {
- impl->tags.erase(impl->tags.begin() + std::ptrdiff_t(i));
- if (i <= impl->editing_index) {
- --impl->editing_index;
- }
- emit tagsEdited();
- found = true;
- break;
- }
-
- if (!impl->tags[i]
- .rect.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value())
- .contains(event->pos())) {
+ for (auto it = std::begin(*impl); it != std::end(*impl); ++it) {
+ if (!it->rect.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value())
+ .contains(event->pos())) {
continue;
}
- if (impl->editing_index == i) {
- impl->moveCursor(impl->text_layout.lineAt(0).xToCursor(
- (event->pos()
- - impl->currentRect()
- .translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value())
- .topLeft())
- .x()),
- false);
- } else {
- impl->editTag(i);
+ if (impl->inCrossArea(it, event->pos())) {
+ impl->removeTag(it);
+ emit tagsEdited();
+ found = true;
+ // TODO This fixes a bug where the scroll bars were not updated after removing a tag
+ event->accept();
+ break;
}
+ impl->editTag(it);
+ impl->moveCursor(impl->lineAt(0).xToCursor(
+ (event->pos()
+ - impl->currentRect()
+ .translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value())
+ .topLeft())
+ .x()),
+ false);
+
found = true;
break;
}
if (!found) {
- for (auto it = std::begin(impl->tags); it != std::end(impl->tags); ++it) {
+ for (auto it = std::begin(*impl); it != std::end(*impl); ++it) {
// Click of a row.
if (it->rect.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value()).bottom()
< event->pos().y()) {
@@ -737,12 +815,12 @@ void TagsEdit::mousePressEvent(QMouseEvent* event)
}
// Last tag of the row.
- auto const row = it->row;
- while (it != std::end(impl->tags) && it->row == row) {
+ const auto row = it->row;
+ while (it != std::end(*impl) && it->row == row) {
++it;
}
- impl->editNewTag(static_cast(std::distance(std::begin(impl->tags), it)));
+ impl->editNewTag(it);
break;
}
@@ -754,7 +832,7 @@ void TagsEdit::mousePressEvent(QMouseEvent* event)
impl->calcRectsAndUpdateScrollRanges();
impl->ensureCursorIsVisibleV();
impl->ensureCursorIsVisibleH();
- impl->updateCursorBlinking();
+ updateCursorBlinking();
viewport()->update();
}
}
@@ -768,18 +846,17 @@ QSize TagsEdit::minimumSizeHint() const
{
ensurePolished();
QFontMetrics fm = fontMetrics();
- QRect rect(0, 0, fm.maxWidth() + tag_cross_padding + tag_cross_width, fm.height() + fm.leading());
- rect += tag_inner + contentsMargins() + viewport()->contentsMargins() + viewportMargins();
+ QRect rect(0, 0, fm.maxWidth() + TAG_CROSS_PADDING + TAG_CROSS_WIDTH, fm.height() + fm.leading());
+ rect += TAG_INNER + contentsMargins() + viewport()->contentsMargins() + viewportMargins();
return rect.size();
}
int TagsEdit::heightForWidth(int w) const
{
- auto const content_width = w;
+ const auto content_width = w;
QRect contents_rect(0, 0, content_width, 100);
contents_rect -= contentsMargins() + viewport()->contentsMargins() + viewportMargins();
- auto tags = impl->tags;
- contents_rect = impl->calcRects(tags, contents_rect);
+ contents_rect = impl->updateTagRenderStates(contents_rect);
contents_rect += contentsMargins() + viewport()->contentsMargins() + viewportMargins();
return contents_rect.height();
}
@@ -793,58 +870,42 @@ void TagsEdit::keyPressEvent(QKeyEvent* event)
impl->selectAll();
event->accept();
} else if (event == QKeySequence::SelectPreviousChar) {
- impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), true);
+ impl->selectPrev();
event->accept();
} else if (event == QKeySequence::SelectNextChar) {
- impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), true);
+ impl->selectNext();
event->accept();
} else if (event == QKeySequence::Paste) {
auto clipboard = QApplication::clipboard();
if (clipboard) {
for (auto tagtext : clipboard->text().split(",")) {
- impl->currentText().insert(impl->cursor, tagtext);
- impl->editNewTag(impl->editing_index + 1);
+ impl->insertText(tagtext);
+ impl->editNextTag(true);
}
}
event->accept();
} else {
switch (event->key()) {
case Qt::Key_Left:
- if (impl->cursor == 0) {
- impl->editPreviousTag();
- } else {
- impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), false);
- }
+ impl->previousCursorPosition();
event->accept();
break;
case Qt::Key_Right:
- if (impl->cursor == impl->currentText().size()) {
- impl->editNextTag();
- } else {
- impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), false);
- }
+ impl->nextCursorPosition();
event->accept();
break;
case Qt::Key_Home:
- if (impl->cursor == 0) {
- impl->editTag(0);
- } else {
- impl->moveCursor(0, false);
- }
+ impl->jumpToFront();
event->accept();
break;
case Qt::Key_End:
- if (impl->cursor == impl->currentText().size()) {
- impl->editTag(impl->tags.size() - 1);
- } else {
- impl->moveCursor(impl->currentText().length(), false);
- }
+ impl->jumpToBack();
event->accept();
break;
case Qt::Key_Backspace:
- if (!impl->currentText().isEmpty()) {
+ if (!impl->isCurrentTagEmpty()) {
impl->removeBackwardOne();
- } else if (impl->editing_index > 0) {
+ } else {
impl->editPreviousTag();
}
event->accept();
@@ -854,13 +915,12 @@ void TagsEdit::keyPressEvent(QKeyEvent* event)
case Qt::Key_Comma:
case Qt::Key_Semicolon:
// If completer is visible, accept the selection or hide if no selection
- if (impl->completer->popup()->isVisible() && impl->completer->popup()->selectionModel()->hasSelection()) {
+ if (completer->popup()->isVisible() && completer->popup()->selectionModel()->hasSelection()) {
break;
}
-
- // Make existing text into a tag
- if (!impl->currentText().isEmpty()) {
- impl->editNewTag(impl->editing_index + 1);
+ // TODO This finishes the tag, but does not split it if the cursor is in the middle of the tag.
+ if (impl->finishTag()) {
+ // TODO Accept event? Original code did not if tag was empty
event->accept();
}
break;
@@ -873,8 +933,7 @@ void TagsEdit::keyPressEvent(QKeyEvent* event)
if (impl->hasSelection()) {
impl->removeSelection();
}
- impl->currentText().insert(impl->cursor, event->text());
- impl->cursor = impl->cursor + event->text().length();
+ impl->insertText(event->text());
event->accept();
}
@@ -884,11 +943,11 @@ void TagsEdit::keyPressEvent(QKeyEvent* event)
impl->calcRectsAndUpdateScrollRanges();
impl->ensureCursorIsVisibleV();
impl->ensureCursorIsVisibleH();
- impl->updateCursorBlinking();
+ updateCursorBlinking();
// complete
- impl->completer->setCompletionPrefix(impl->currentText());
- impl->completer->complete();
+ completer->setCompletionPrefix(impl->currentText());
+ completer->complete();
viewport()->update();
@@ -896,31 +955,16 @@ void TagsEdit::keyPressEvent(QKeyEvent* event)
}
}
-void TagsEdit::completion(QStringList const& completions)
+void TagsEdit::setCompletion(const QStringList& completions)
{
- impl->completer = std::make_unique([&] {
- QStringList ret;
- std::copy(completions.begin(), completions.end(), std::back_inserter(ret));
- return ret;
- }());
- impl->setupCompleter();
+ completer.reset(new QCompleter(completions));
+ setupCompleter();
}
-void TagsEdit::tags(QStringList const& tags)
+void TagsEdit::setTags(const QStringList& tags)
{
- // Set to Default-state.
- impl->editing_index = 0;
- QList t{Tag()};
+ impl->setTags(tags.begin(), tags.end());
- std::transform(EmptySkipIterator(tags.begin(), tags.end()), // Ensure Invariant-1
- EmptySkipIterator(tags.end()),
- std::back_inserter(t),
- [](QString const& text) {
- return Tag{text, QRect(), 0};
- });
-
- impl->tags = std::move(t);
- impl->editNewTag(impl->tags.size());
impl->updateDisplayText();
impl->calcRectsAndUpdateScrollRanges();
viewport()->update();
@@ -930,17 +974,18 @@ void TagsEdit::tags(QStringList const& tags)
QStringList TagsEdit::tags() const
{
QStringList ret;
- std::transform(EmptySkipIterator(impl->tags.begin(), impl->tags.end()),
- EmptySkipIterator(impl->tags.end()),
- std::back_inserter(ret),
- [](Tag const& tag) { return tag.text; });
+ for (const auto& tag : *impl) {
+ if (!tag.isEmpty()) {
+ ret.push_back(tag.text);
+ }
+ }
return ret;
}
void TagsEdit::mouseMoveEvent(QMouseEvent* event)
{
if (!m_readOnly) {
- for (int i = 0; i < impl->tags.size(); ++i) {
+ for (auto i = std::begin(*impl); i != std::end(*impl); ++i) {
if (impl->inCrossArea(i, event->pos())) {
viewport()->setCursor(Qt::ArrowCursor);
return;
@@ -980,3 +1025,11 @@ bool TagsEdit::isAcceptableInput(const QKeyEvent* event) const
return false;
}
+
+void TagsEdit::setupCompleter()
+{
+ completer->setWidget(this);
+ connect(completer.get(),
+ static_cast(&QCompleter::activated),
+ [this](QString const& text) { impl->setCurrentText(text); });
+}
diff --git a/src/gui/tag/TagsEdit.h b/src/gui/tag/TagsEdit.h
index 44297fb34..1ee88b9f3 100644
--- a/src/gui/tag/TagsEdit.h
+++ b/src/gui/tag/TagsEdit.h
@@ -25,12 +25,10 @@
#pragma once
#include
+#include
-#include
-#include
+class QCompleter;
-/// Tag multi-line editor widget
-/// `Space` commits a tag and initiates a new tag edition
class TagsEdit : public QAbstractScrollArea
{
Q_OBJECT
@@ -39,27 +37,20 @@ public:
explicit TagsEdit(QWidget* parent = nullptr);
~TagsEdit() override;
- // QWidget
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
int heightForWidth(int w) const override;
- /// Set completions
- void completion(QStringList const& completions);
-
- /// Set tags
- void tags(QStringList const& tags);
-
- /// Get tags
- QStringList tags() const;
-
void setReadOnly(bool readOnly);
+ void setCompletion(const QStringList& completions);
+ void setTags(const QStringList& tags);
+
+ QStringList tags() const;
signals:
void tagsEdited();
protected:
- // QWidget
void paintEvent(QPaintEvent* event) override;
void timerEvent(QTimerEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
@@ -72,8 +63,16 @@ protected:
private:
bool isAcceptableInput(QKeyEvent const* event) const;
+ void setupCompleter();
+ void setCursorVisible(bool visible);
+ bool cursorVisible() const;
+ void updateCursorBlinking();
struct Impl;
- std::unique_ptr impl;
- bool m_readOnly;
+ QScopedPointer impl;
+ QScopedPointer completer;
+
+ bool m_readOnly = false;
+ int blink_timer = 0;
+ bool blink_status = true;
};
diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp
index f54c97137..1aa20e6f1 100644
--- a/tests/gui/TestGui.cpp
+++ b/tests/gui/TestGui.cpp
@@ -2421,7 +2421,7 @@ void TestGui::addCannedEntries()
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QTest::keyClicks(titleEdit, "test");
auto* editEntryWidgetTagsEdit = editEntryWidget->findChild("tagsList");
- editEntryWidgetTagsEdit->tags(QStringList() << "testTag");
+ editEntryWidgetTagsEdit->setTags({"testTag"});
auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);