feat: comprehensive Wino Mail updates
- ReaderView: complete rewrite with CSS styling, headers, attachments, zoom, find, dark mode, image blocking - Categories: DAO + UI tree widget with colors, nested categories, assignment - Rules engine: DAO + evaluation + actions (move, mark read, flag, delete, category, forward) - Templates: DAO + editor + manager with variables support - Signatures: DAO + manager + auto-per-account + manual selector in compose - ComposeView: signature combo, template selector, auto-signature on new email - MainWindow: frameless with rounded corners, 1px border, resize from edges, no status bar - Database: new tables (Category, MailCategory, Rule, Template, Signature) with indexes
This commit is contained in:
Vendored
+134
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 Nicolai Trandafil
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QFontMetrics>
|
||||
#include <QMargins>
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QSize>
|
||||
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
struct Tag {
|
||||
QString text;
|
||||
QRect rect;
|
||||
|
||||
bool operator==(Tag const& rhs) const {
|
||||
return text == rhs.text && rect == rhs.rect;
|
||||
}
|
||||
};
|
||||
|
||||
struct StyleConfig {
|
||||
/// Padding from the text to the the pill border
|
||||
QMargins pill_thickness = {7, 7, 8, 7};
|
||||
|
||||
/// Space between pills
|
||||
int pills_h_spacing = 7;
|
||||
|
||||
/// Space between rows of pills (for multi line tags)
|
||||
int tag_v_spacing = 2;
|
||||
|
||||
/// Size of cross side
|
||||
qreal tag_cross_size = 8;
|
||||
|
||||
/// Distance between text and the cross
|
||||
int tag_cross_spacing = 3;
|
||||
|
||||
QColor color{255, 164, 100, 100};
|
||||
|
||||
/// Rounding of the pill
|
||||
qreal rounding_x_radius = 5;
|
||||
|
||||
/// Rounding of the pill
|
||||
qreal rounding_y_radius = 5;
|
||||
|
||||
/// Calculate the width that a tag would have with the given text width
|
||||
int pillWidth(int text_width, bool has_cross) const {
|
||||
return text_width + pill_thickness.left() + (has_cross ? (tag_cross_spacing + tag_cross_size) : 0) +
|
||||
pill_thickness.right();
|
||||
}
|
||||
|
||||
/// Calculate the height that a tag would have with the given text height
|
||||
int pillHeight(int text_height) const {
|
||||
return text_height + pill_thickness.top() + pill_thickness.bottom();
|
||||
}
|
||||
|
||||
/// \param fit When nullopt arranges the tags in a line
|
||||
void calcRects(QPoint& lt, std::vector<Tag>& tags, QFontMetrics const& fm, std::optional<QRect> const& fit,
|
||||
bool has_cross) const;
|
||||
|
||||
void drawTags(QPainter& p, std::vector<Tag> const& tags, QFontMetrics const& fm, QPoint const& translate,
|
||||
bool has_cross) const;
|
||||
|
||||
std::string debugString() const {
|
||||
std::ostringstream os;
|
||||
os << "StyleConfig{"
|
||||
<< "pill_thickness: "
|
||||
<< "QMargins{"
|
||||
<< "left: " << pill_thickness.left() << "; "
|
||||
<< "top: " << pill_thickness.top() << "; "
|
||||
<< "right: " << pill_thickness.right() << "; "
|
||||
<< "bottom: " << pill_thickness.bottom() << "}; "
|
||||
<< "pills_h_spacing: " << pills_h_spacing << "; "
|
||||
<< "tag_v_spacing: " << tag_v_spacing << "; "
|
||||
<< "tag_cross_size: " << tag_cross_size << "; "
|
||||
<< "tag_cross_spacing: " << tag_cross_spacing << "; "
|
||||
<< "color: rgba(" << color.red() << ", " << color.green() << ", " << color.blue() << ", " << color.alpha()
|
||||
<< "); "
|
||||
<< "rounding_x_radius: " << rounding_x_radius << "; "
|
||||
<< "rounding_y_radius: " << rounding_y_radius << "}";
|
||||
return os.str();
|
||||
}
|
||||
};
|
||||
|
||||
struct BehaviorConfig {
|
||||
/// Maintain only unique tags
|
||||
bool unique = true;
|
||||
bool restore_cursor_position_on_focus_click = false;
|
||||
bool read_only = false;
|
||||
|
||||
std::string debugString() const {
|
||||
std::ostringstream os;
|
||||
os << "BehaviorConfig{"
|
||||
<< "unique: " << unique << "; "
|
||||
<< "restore_cursor_position_on_focus_click: " << restore_cursor_position_on_focus_click << "; "
|
||||
<< "read_only: " << read_only << "}";
|
||||
return os.str();
|
||||
}
|
||||
};
|
||||
|
||||
struct Config {
|
||||
StyleConfig style{};
|
||||
BehaviorConfig behavior{};
|
||||
};
|
||||
|
||||
} // namespace everload_tags
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Nicolai Trandafil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <QAbstractScrollArea>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
/// Tag multi-line tags editor widget, similar to `QTextEdit`.
|
||||
/// `Space` commits a tag and initiates a new tag edition.
|
||||
class TagsEdit : public QAbstractScrollArea {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// \param unique Ensure tags uniqueness
|
||||
explicit TagsEdit(QWidget* parent = nullptr, Config config = {});
|
||||
~TagsEdit() override;
|
||||
|
||||
// QWidget
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
int heightForWidth(int w) const override;
|
||||
|
||||
/// Set completions
|
||||
void completion(std::vector<QString> const& completions);
|
||||
void completion(QStringList const& completions);
|
||||
|
||||
/// Set tags
|
||||
void tags(std::vector<QString> const& tags);
|
||||
void tags(QStringList const& tags);
|
||||
|
||||
/// Get tags
|
||||
std::vector<QString> tags() const;
|
||||
QStringList tags2() const;
|
||||
|
||||
/// Set config
|
||||
void config(Config config);
|
||||
|
||||
/// Get config
|
||||
Config config() const;
|
||||
|
||||
signals:
|
||||
void tagsEdited();
|
||||
|
||||
protected:
|
||||
// QWidget
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void timerEvent(QTimerEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void focusInEvent(QFocusEvent* event) override;
|
||||
void focusOutEvent(QFocusEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
} // namespace everload_tags
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Nicolai Trandafil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
/// Single line tag editor widget, simial to `QLineEdit`.
|
||||
/// `Space` commits a tag and initiates a new tag edition.
|
||||
class TagsLineEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TagsLineEdit(QWidget* parent = nullptr, Config config = {});
|
||||
~TagsLineEdit() override;
|
||||
|
||||
// QWidget
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
/// Set completions
|
||||
void completion(std::vector<QString> const& completions);
|
||||
void completion(QStringList const& completions);
|
||||
|
||||
/// Set tags
|
||||
void tags(std::vector<QString> const& tags);
|
||||
void tags(QStringList const& tags);
|
||||
|
||||
/// Get tags
|
||||
std::vector<QString> tags() const;
|
||||
QStringList tags2() const;
|
||||
|
||||
/// Set config
|
||||
void config(Config config);
|
||||
|
||||
/// Get config
|
||||
Config config() const;
|
||||
|
||||
signals:
|
||||
void tagsEdited();
|
||||
|
||||
protected:
|
||||
// QWidget
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void timerEvent(QTimerEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void focusInEvent(QFocusEvent* event) override;
|
||||
void focusOutEvent(QFocusEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
} // namespace everload_tags
|
||||
Vendored
+428
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021 Nicolai Trandafil
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QFontMetrics>
|
||||
#include <QGuiApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
#include <QStyleHints>
|
||||
#include <QStyleOptionFrame>
|
||||
#include <QTextLayout>
|
||||
|
||||
#include <chrono>
|
||||
#include "config.hpp"
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
|
||||
#define FONT_METRICS_WIDTH(fmt, ...) fmt.width(__VA_ARGS__)
|
||||
#else
|
||||
#define FONT_METRICS_WIDTH(fmt, ...) fmt.horizontalAdvance(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
struct Style : StyleConfig {
|
||||
static QRectF crossRect(QRectF const& r, qreal cross_size) {
|
||||
QRectF cross(QPointF{0, 0}, QSizeF{cross_size, cross_size});
|
||||
cross.moveCenter(QPointF(r.right() - cross_size, r.center().y()));
|
||||
return cross;
|
||||
}
|
||||
|
||||
QRectF crossRect(QRectF const& r) const {
|
||||
return crossRect(r, tag_cross_size);
|
||||
}
|
||||
|
||||
template <std::ranges::output_range<Tag> Range>
|
||||
static void calcRects(QPoint& lt, Range&& tags, StyleConfig const& style, QFontMetrics const& fm,
|
||||
std::optional<QRect> const& fit, bool has_cross) {
|
||||
for (auto& tag : tags) {
|
||||
auto const text_width = FONT_METRICS_WIDTH(fm, tag.text);
|
||||
QRect rect(lt, QSize(style.pillWidth(text_width, has_cross), style.pillHeight(fm.height())));
|
||||
|
||||
if (fit) {
|
||||
if (fit->right() < rect.right() && // doesn't fit in current line
|
||||
rect.left() != fit->left() // doesn't occupy entire line already
|
||||
) {
|
||||
rect.moveTo(fit->left(), rect.bottom() + style.tag_v_spacing);
|
||||
lt = rect.topLeft();
|
||||
}
|
||||
}
|
||||
|
||||
tag.rect = rect;
|
||||
lt.setX(rect.right() + style.pills_h_spacing);
|
||||
}
|
||||
}
|
||||
|
||||
template <std::ranges::output_range<Tag> Range>
|
||||
void calcRects(QPoint& lt, Range&& tags, QFontMetrics const& fm, std::optional<QRect> const& fit = std::nullopt,
|
||||
bool has_cross = true) const {
|
||||
calcRects(lt, tags, *this, fm, fit, has_cross);
|
||||
}
|
||||
|
||||
template <std::ranges::input_range Range>
|
||||
static void drawTags(QPainter& p, Range&& tags, StyleConfig const& style, QFontMetrics const& fm,
|
||||
QPoint const& offset, bool has_cross) {
|
||||
for (auto const& tag : tags) {
|
||||
QRect const& i_r = tag.rect.translated(offset);
|
||||
auto const text_pos =
|
||||
i_r.topLeft() + QPointF(style.pill_thickness.left(), fm.ascent() + ((i_r.height() - fm.height()) / 2));
|
||||
|
||||
// draw tag rect
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(i_r, style.rounding_x_radius, style.rounding_y_radius);
|
||||
p.fillPath(path, style.color);
|
||||
|
||||
// draw text
|
||||
p.drawText(text_pos, tag.text);
|
||||
|
||||
if (has_cross) {
|
||||
auto const i_cross_r = crossRect(i_r, style.tag_cross_size);
|
||||
|
||||
QPen pen = p.pen();
|
||||
pen.setWidth(2);
|
||||
|
||||
p.save();
|
||||
p.setPen(pen);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.drawLine(QLineF(i_cross_r.topLeft(), i_cross_r.bottomRight()));
|
||||
p.drawLine(QLineF(i_cross_r.bottomLeft(), i_cross_r.topRight()));
|
||||
p.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <std::ranges::input_range Range>
|
||||
void drawTags(QPainter& p, Range&& tags, QFontMetrics const& fm, QPoint const& offset,
|
||||
bool has_cross = true) const {
|
||||
drawTags(p, tags, *this, fm, offset, has_cross);
|
||||
}
|
||||
};
|
||||
|
||||
struct Behavior : BehaviorConfig {
|
||||
using BehaviorConfig::unique; /// Turn on/off Invariant-2
|
||||
};
|
||||
|
||||
// Invariant-1 no empty tags apart from currently being edited.
|
||||
// Invariant-2 tags are unique.
|
||||
// Default-state is one empty tag which is editing.
|
||||
struct State {
|
||||
std::vector<Tag> tags{Tag{}};
|
||||
size_t editing_index{0};
|
||||
int blink_timer{0};
|
||||
bool blink_status{true};
|
||||
int cursor{0};
|
||||
int select_start{0};
|
||||
int select_size{0};
|
||||
QTextLayout text_layout;
|
||||
std::unique_ptr<QCompleter> completer{new QCompleter{}};
|
||||
std::chrono::steady_clock::time_point focused_at{};
|
||||
|
||||
QRect const& editorRect() const {
|
||||
return tags[editing_index].rect;
|
||||
}
|
||||
|
||||
QString const& editorText() const {
|
||||
return tags[editing_index].text;
|
||||
}
|
||||
|
||||
QString& editorText() {
|
||||
return tags[editing_index].text;
|
||||
}
|
||||
|
||||
void updateCursorBlinking(QObject* ifce) {
|
||||
setCursorVisible(blink_timer, ifce);
|
||||
}
|
||||
|
||||
void updateDisplayText() {
|
||||
text_layout.clearLayout();
|
||||
text_layout.setText(editorText());
|
||||
text_layout.beginLayout();
|
||||
text_layout.createLine();
|
||||
text_layout.endLayout();
|
||||
}
|
||||
|
||||
void setCursorVisible(bool visible, QObject* ifce) {
|
||||
if (blink_timer) {
|
||||
ifce->killTimer(blink_timer);
|
||||
blink_timer = 0;
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
blink_status = true;
|
||||
int flashTime = QGuiApplication::styleHints()->cursorFlashTime();
|
||||
if (flashTime >= 2) {
|
||||
blink_timer = ifce->startTimer(flashTime / 2);
|
||||
}
|
||||
} else {
|
||||
blink_status = false;
|
||||
}
|
||||
}
|
||||
|
||||
QVector<QTextLayout::FormatRange> formatting(QPalette const& palette) const {
|
||||
if (select_size == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QTextLayout::FormatRange selection;
|
||||
selection.start = select_start;
|
||||
selection.length = select_size;
|
||||
selection.format.setBackground(palette.brush(QPalette::Highlight));
|
||||
selection.format.setForeground(palette.brush(QPalette::HighlightedText));
|
||||
return {selection};
|
||||
}
|
||||
|
||||
qreal cursorToX() {
|
||||
return text_layout.lineAt(0).cursorToX(cursor);
|
||||
}
|
||||
|
||||
void moveCursor(int pos, bool mark) {
|
||||
if (mark) {
|
||||
auto e = select_start + select_size;
|
||||
int anchor = select_size > 0 && cursor == select_start ? e
|
||||
: select_size > 0 && cursor == e ? select_start
|
||||
: cursor;
|
||||
select_start = qMin(anchor, pos);
|
||||
select_size = qMax(anchor, pos) - select_start;
|
||||
} else {
|
||||
deselectAll();
|
||||
}
|
||||
cursor = pos;
|
||||
}
|
||||
|
||||
void deselectAll() {
|
||||
select_start = 0;
|
||||
select_size = 0;
|
||||
}
|
||||
|
||||
bool hasSelection() const noexcept {
|
||||
return select_size > 0;
|
||||
}
|
||||
|
||||
void selectAll() {
|
||||
select_start = 0;
|
||||
select_size = editorText().size();
|
||||
}
|
||||
|
||||
void removeSelection() {
|
||||
assert(select_start + select_size <= editorText().size());
|
||||
cursor = select_start;
|
||||
editorText().remove(cursor, select_size);
|
||||
deselectAll();
|
||||
}
|
||||
|
||||
void removeBackwardOne() {
|
||||
if (hasSelection()) {
|
||||
removeSelection();
|
||||
} else {
|
||||
editorText().remove(--cursor, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void removeDuplicates() {
|
||||
everload_tags::removeDuplicates(tags);
|
||||
auto const it = std::find_if(tags.begin(), tags.end(), [](auto const& x) {
|
||||
return x.text.isEmpty(); // Thanks to Invariant-1 we can track back the editing_index.
|
||||
});
|
||||
assert(it != tags.end());
|
||||
editing_index = static_cast<size_t>(std::distance(tags.begin(), it));
|
||||
}
|
||||
};
|
||||
|
||||
struct Common : Style, Behavior, State {
|
||||
void drawEditor(QPainter& p, QPalette const& palette, QPoint const& offset) const {
|
||||
auto const& r = editorRect();
|
||||
auto const& txt_p = r.topLeft() + QPointF(pill_thickness.left(), pill_thickness.top());
|
||||
auto const f = formatting(palette);
|
||||
text_layout.draw(&p, txt_p - offset, f);
|
||||
if (blink_status) {
|
||||
text_layout.drawCursor(&p, txt_p - offset, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
bool inCrossArea(size_t tag_index, QPoint const& point, QPoint const& offset) const {
|
||||
return crossRect(tags[tag_index].rect).adjusted(-1, -1, 1, 1).translated(-offset).contains(point) &&
|
||||
(!cursorVisible() || tag_index != editing_index);
|
||||
}
|
||||
|
||||
bool isCurrentTagADuplicate() const {
|
||||
assert(editing_index < tags.size());
|
||||
auto const mid = tags.begin() + static_cast<std::ptrdiff_t>(editing_index);
|
||||
auto const text_eq = [this](auto const& x) { return x.text == editorText(); };
|
||||
return std::find_if(tags.begin(), mid, text_eq) != mid ||
|
||||
std::find_if(mid + 1, tags.end(), text_eq) != tags.end();
|
||||
}
|
||||
|
||||
/// Makes the tag at `i` currently editing, and ensures Invariant-1 and Invariant-2`.
|
||||
void setEditorIndex(size_t i) {
|
||||
assert(i < tags.size());
|
||||
if (editorText().isEmpty() || (unique && isCurrentTagADuplicate())) {
|
||||
tags.erase(std::next(begin(tags), static_cast<std::ptrdiff_t>(editing_index)));
|
||||
if (editing_index <= i) { // Did we shift `i`?
|
||||
--i;
|
||||
}
|
||||
}
|
||||
editing_index = i;
|
||||
}
|
||||
|
||||
// Inserts a new tag at `i`, makes the tag currently editing, and ensures Invariant-1.
|
||||
void editNewTag(size_t i) {
|
||||
assert(i <= tags.size());
|
||||
tags.insert(begin(tags) + static_cast<std::ptrdiff_t>(i), Tag{});
|
||||
if (i <= editing_index) { // Did we shift `editing_index`?
|
||||
++editing_index;
|
||||
}
|
||||
setEditorIndex(i);
|
||||
moveCursor(0, false);
|
||||
}
|
||||
|
||||
void editPreviousTag() {
|
||||
if (editing_index > 0) {
|
||||
setEditorIndex(editing_index - 1);
|
||||
moveCursor(editorText().size(), false);
|
||||
}
|
||||
}
|
||||
|
||||
void editNextTag() {
|
||||
if (editing_index < tags.size() - 1) {
|
||||
setEditorIndex(editing_index + 1);
|
||||
moveCursor(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
void editTag(size_t i) {
|
||||
assert(i < tags.size());
|
||||
setEditorIndex(i);
|
||||
moveCursor(editorText().size(), false);
|
||||
}
|
||||
|
||||
void removeTag(size_t i) {
|
||||
tags.erase(tags.begin() + static_cast<ptrdiff_t>(i));
|
||||
if (i <= editing_index) {
|
||||
--editing_index;
|
||||
}
|
||||
}
|
||||
|
||||
void setTags(std::ranges::forward_range auto const& tags) {
|
||||
std::unordered_set<QString> unique_tags;
|
||||
std::vector<Tag> t;
|
||||
for (auto const& x : tags) {
|
||||
if (/* Invariant-1 */ !x.isEmpty() && /* Invariant-2 */ (!unique || unique_tags.insert(x).second)) {
|
||||
t.emplace_back(Tag{x, QRect{}});
|
||||
}
|
||||
}
|
||||
this->tags = std::move(t);
|
||||
this->tags.push_back(Tag{});
|
||||
editing_index = this->tags.size() - 1;
|
||||
moveCursor(0, false);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void getTags(T& out) {
|
||||
out.resize(tags.size());
|
||||
std::transform(tags.begin(), tags.end(), out.begin(), [](auto const& tag) { return tag.text; });
|
||||
if (editorText().isEmpty() || (unique && std::count(out.begin(), out.end(), editorText()) > 1)) {
|
||||
out.erase(out.begin() + static_cast<ptrdiff_t>(editing_index));
|
||||
}
|
||||
}
|
||||
|
||||
bool cursorVisible() const {
|
||||
return !read_only && blink_timer;
|
||||
}
|
||||
|
||||
enum class HandleClickResult {
|
||||
unhandled,
|
||||
removed_a_tag,
|
||||
moved_cursor_in_a_tag,
|
||||
moved_cursor_across_tags,
|
||||
};
|
||||
|
||||
HandleClickResult handleClick(QMouseEvent* event, QPoint const& offset) {
|
||||
auto const it = std::find_if(begin(tags), end(tags), [&](auto const& tag) {
|
||||
return tag.rect.translated(-offset).contains(event->pos());
|
||||
});
|
||||
if (it != end(tags)) {
|
||||
auto const i = static_cast<size_t>(std::distance(begin(tags), it));
|
||||
|
||||
if (inCrossArea(i, event->pos(), offset)) {
|
||||
removeTag(i);
|
||||
return HandleClickResult::removed_a_tag;
|
||||
} else if (editing_index == i) {
|
||||
moveCursor(text_layout.lineAt(0).xToCursor(
|
||||
(event->pos() - (editorRect() - pill_thickness).translated(-offset).topLeft()).x()),
|
||||
false);
|
||||
return HandleClickResult::moved_cursor_in_a_tag;
|
||||
} else {
|
||||
editTag(i);
|
||||
return HandleClickResult::moved_cursor_across_tags;
|
||||
}
|
||||
}
|
||||
|
||||
return HandleClickResult::unhandled;
|
||||
}
|
||||
};
|
||||
|
||||
/// \ref `bool QInputControl::isAcceptableInput(QKeyEvent const* event) const`
|
||||
inline bool isAcceptableInput(QKeyEvent const& event) {
|
||||
auto const text = event.text();
|
||||
if (text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const c = text.at(0);
|
||||
|
||||
if (c.category() == QChar::Other_Format) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.modifiers() == Qt::ControlModifier || event.modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.isPrint()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c.category() == QChar::Other_PrivateUse) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline auto elapsed(std::chrono::steady_clock::time_point const& ts) {
|
||||
return std::chrono::steady_clock::now() - ts;
|
||||
}
|
||||
|
||||
} // namespace everload_tags
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
#include "config.hpp"
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
void StyleConfig::calcRects(QPoint& lt, std::vector<Tag>& tags, QFontMetrics const& fm, std::optional<QRect> const& fit,
|
||||
bool has_cross) const {
|
||||
Style::calcRects(lt, tags, *this, fm, fit, has_cross);
|
||||
}
|
||||
|
||||
void StyleConfig::drawTags(QPainter& p, std::vector<Tag> const& tags, QFontMetrics const& fm, QPoint const& offset,
|
||||
bool has_cross) const {
|
||||
Style::drawTags(p, tags, *this, fm, offset, has_cross);
|
||||
}
|
||||
|
||||
} // namespace everload_tags
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
<RCC/>
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 Nicolai Trandafil
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
template <class Fn>
|
||||
struct ScopeExit : Fn {
|
||||
~ScopeExit() {
|
||||
try {
|
||||
(*this)();
|
||||
} catch (...) {
|
||||
qDebug() << "exception durring scope exit";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct MakeScopeExit {
|
||||
template <class Fn>
|
||||
auto operator->*(Fn&& fn) const {
|
||||
return ScopeExit<Fn>{std::forward<Fn>(fn)};
|
||||
}
|
||||
};
|
||||
|
||||
template <class Fn>
|
||||
struct ScopeFail : Fn {
|
||||
~ScopeFail() {
|
||||
if (std::current_exception()) {
|
||||
try {
|
||||
(*this)();
|
||||
} catch (...) {
|
||||
qDebug() << "exception during scope fail";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct MakeScopeFail {
|
||||
template <class Fn>
|
||||
auto operator->*(Fn&& fn) const {
|
||||
return ScopeFail<Fn>{std::forward<Fn>(fn)};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace everload_tags
|
||||
|
||||
#define EVERLOAD_TAGS_CONCATENATE_IMPL(s1, s2) s1##s2
|
||||
|
||||
#define EVERLOAD_TAGS_CONCATENATE(s1, s2) EVERLOAD_TAGS_CONCATENATE_IMPL(s1, s2)
|
||||
|
||||
#define EVERLOAD_TAGS_UNIQUE_IDENTIFIER EVERLOAD_TAGS_CONCATENATE(UNIQUE_IDENTIFIER_, __LINE__)
|
||||
|
||||
#define EVERLOAD_TAGS_SCOPE_EXIT auto const EVERLOAD_TAGS_UNIQUE_IDENTIFIER = everload_tags::MakeScopeExit{}->*[&]
|
||||
|
||||
#define EVERLOAD_TAGS_SCOPE_FAIL auto const EVERLOAD_TAGS_UNIQUE_IDENTIFIER = everload_tags::MakeScopeFail{}->*[&]
|
||||
Vendored
+507
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Nicolai Trandafil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "tags_edit.hpp"
|
||||
|
||||
#include "common.hpp"
|
||||
#include "scope_exit.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QKeyEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QScrollBar>
|
||||
#include <QStyle>
|
||||
#include <QStyleHints>
|
||||
#include <QStyleOptionFrame>
|
||||
#include <QTextLayout>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
struct TagsEdit::Impl : Common {
|
||||
explicit Impl(TagsEdit* ifce, Config config) : Common{{config.style}, {config.behavior}, {}}, ifce{ifce} {}
|
||||
|
||||
QPoint offset() const {
|
||||
return QPoint{ifce->horizontalScrollBar()->value(), ifce->verticalScrollBar()->value()};
|
||||
}
|
||||
|
||||
using Common::drawTags;
|
||||
|
||||
template <std::ranges::input_range Range>
|
||||
void drawTags(QPainter& p, Range range) const {
|
||||
drawTags(p, range, ifce->fontMetrics(), -offset(),
|
||||
!read_only && (!restore_cursor_position_on_focus_click || ifce->hasFocus()));
|
||||
}
|
||||
|
||||
void setEditorText(QString const& text) {
|
||||
editorText() = text;
|
||||
moveCursor(editorText().length(), false);
|
||||
update1();
|
||||
}
|
||||
|
||||
void setupCompleter() {
|
||||
completer->setWidget(ifce);
|
||||
QObject::connect(completer.get(), qOverload<QString const&>(&QCompleter::activated),
|
||||
[this](QString const& text) { setEditorText(text); });
|
||||
}
|
||||
|
||||
using Common::calcRects;
|
||||
|
||||
void calcRects(QRect r, QPoint& lt, QFontMetrics const& fm) {
|
||||
auto const middle = tags.begin() + static_cast<ptrdiff_t>(editing_index);
|
||||
|
||||
calcRects(lt, std::ranges::subrange(tags.begin(), middle), fm, r, !read_only);
|
||||
|
||||
if (cursorVisible() || !editorText().isEmpty()) {
|
||||
calcRects(lt, std::ranges::subrange(middle, middle + 1), fm, r, !read_only);
|
||||
}
|
||||
|
||||
calcRects(lt, std::ranges::subrange(middle + 1, tags.end()), fm, r, !read_only);
|
||||
}
|
||||
|
||||
QRect calcRects(QRect r) {
|
||||
auto lt = r.topLeft();
|
||||
auto const fm = ifce->fontMetrics();
|
||||
calcRects(r, lt, fm);
|
||||
r.setBottom(lt.y() + pillHeight(fm.height()) - 1);
|
||||
return r;
|
||||
}
|
||||
|
||||
QRect calcRects() {
|
||||
return calcRects(contentsRect());
|
||||
}
|
||||
|
||||
QRect contentsRect() const {
|
||||
return ifce->viewport()->contentsRect();
|
||||
}
|
||||
|
||||
void calcRectsUpdateScrollRanges() {
|
||||
calcRects();
|
||||
updateVScrollRange();
|
||||
updateHScrollRange();
|
||||
}
|
||||
|
||||
void updateVScrollRange() {
|
||||
if (tags.size() == 1 && tags.front().text.isEmpty()) {
|
||||
ifce->verticalScrollBar()->setRange(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
auto const fm = ifce->fontMetrics();
|
||||
auto const row_h = pillHeight(fm.height()) + tag_v_spacing;
|
||||
ifce->verticalScrollBar()->setPageStep(row_h);
|
||||
assert(!tags.empty()); // Invariant-1
|
||||
|
||||
int top = tags.front().rect.top();
|
||||
int bottom = tags.back().rect.bottom();
|
||||
|
||||
if (editing_index == 0 && !(cursorVisible() || !editorText().isEmpty())) {
|
||||
top = tags[1].rect.top();
|
||||
} else if (editing_index == tags.size() - 1 && !(cursorVisible() || !editorText().isEmpty())) {
|
||||
bottom = tags[tags.size() - 2].rect.bottom();
|
||||
}
|
||||
|
||||
auto const h = bottom - top + 1;
|
||||
auto const contents_rect = contentsRect();
|
||||
|
||||
if (contents_rect.height() < h) {
|
||||
ifce->verticalScrollBar()->setRange(0, h - contents_rect.height());
|
||||
} else {
|
||||
ifce->verticalScrollBar()->setRange(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void updateHScrollRange() {
|
||||
assert(!tags.empty()); // Invariant-1
|
||||
auto const width = std::max_element(begin(tags), end(tags), [](auto const& x, auto const& y) {
|
||||
return x.rect.width() < y.rect.width();
|
||||
})->rect.width();
|
||||
|
||||
auto const contents_rect_width = contentsRect().width();
|
||||
|
||||
if (contents_rect_width < width) {
|
||||
ifce->horizontalScrollBar()->setRange(0, width - contents_rect_width);
|
||||
} else {
|
||||
ifce->horizontalScrollBar()->setRange(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void ensureCursorIsVisibleV() {
|
||||
if (!cursorVisible()) {
|
||||
return;
|
||||
}
|
||||
auto const fm = ifce->fontMetrics();
|
||||
auto const row_h = pillHeight(fm.height());
|
||||
auto const vscroll = ifce->verticalScrollBar()->value();
|
||||
auto const cursor_top = editorRect().topLeft() + QPoint(qRound(cursorToX()), 0);
|
||||
auto const cursor_bottom = cursor_top + QPoint(0, row_h - 1);
|
||||
auto const 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()) {
|
||||
ifce->verticalScrollBar()->setValue(cursor_top.y() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void ensureCursorIsVisibleH() {
|
||||
if (!cursorVisible()) {
|
||||
return;
|
||||
}
|
||||
auto const contents_rect = contentsRect().translated(ifce->horizontalScrollBar()->value(), 0);
|
||||
auto const cursor_x = (editorRect() - pill_thickness).left() + qRound(cursorToX());
|
||||
if (contents_rect.right() < cursor_x) {
|
||||
ifce->horizontalScrollBar()->setValue(cursor_x - contents_rect.width());
|
||||
} else if (cursor_x < contents_rect.left()) {
|
||||
ifce->horizontalScrollBar()->setValue(cursor_x - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void update1(bool keep_cursor_visible = true) {
|
||||
updateDisplayText();
|
||||
calcRectsUpdateScrollRanges();
|
||||
if (keep_cursor_visible) {
|
||||
ensureCursorIsVisibleV();
|
||||
ensureCursorIsVisibleH();
|
||||
}
|
||||
updateCursorBlinking(ifce);
|
||||
ifce->viewport()->update();
|
||||
}
|
||||
|
||||
TagsEdit* const ifce;
|
||||
};
|
||||
|
||||
TagsEdit::TagsEdit(QWidget* parent, Config config)
|
||||
: QAbstractScrollArea(parent), impl(std::make_unique<Impl>(this, config)) {
|
||||
QSizePolicy size_policy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||
size_policy.setHeightForWidth(true);
|
||||
setSizePolicy(size_policy);
|
||||
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
viewport()->setCursor(Qt::IBeamCursor);
|
||||
setAttribute(Qt::WA_InputMethodEnabled, true);
|
||||
setMouseTracking(true);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
impl->setupCompleter();
|
||||
impl->setCursorVisible(hasFocus(), this);
|
||||
impl->updateDisplayText();
|
||||
|
||||
viewport()->setContentsMargins(1, 1, 1, 1);
|
||||
}
|
||||
|
||||
TagsEdit::~TagsEdit() = default;
|
||||
|
||||
void TagsEdit::resizeEvent(QResizeEvent* event) {
|
||||
QAbstractScrollArea::resizeEvent(event);
|
||||
impl->calcRectsUpdateScrollRanges();
|
||||
}
|
||||
|
||||
void TagsEdit::focusInEvent(QFocusEvent* event) {
|
||||
QAbstractScrollArea::focusInEvent(event);
|
||||
impl->focused_at = std::chrono::steady_clock::now();
|
||||
impl->setCursorVisible(true, this);
|
||||
impl->updateDisplayText();
|
||||
impl->calcRectsUpdateScrollRanges();
|
||||
if (event->reason() != Qt::FocusReason::MouseFocusReason || impl->restore_cursor_position_on_focus_click) {
|
||||
impl->ensureCursorIsVisibleH();
|
||||
impl->ensureCursorIsVisibleV();
|
||||
}
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void TagsEdit::focusOutEvent(QFocusEvent* event) {
|
||||
QAbstractScrollArea::focusOutEvent(event);
|
||||
impl->setCursorVisible(false, this);
|
||||
impl->updateDisplayText();
|
||||
impl->calcRectsUpdateScrollRanges();
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void TagsEdit::paintEvent(QPaintEvent* e) {
|
||||
QAbstractScrollArea::paintEvent(e);
|
||||
|
||||
QPainter p(viewport());
|
||||
|
||||
p.setClipRect(impl->contentsRect());
|
||||
|
||||
auto const middle = impl->tags.cbegin() + static_cast<ptrdiff_t>(impl->editing_index);
|
||||
|
||||
// tags
|
||||
impl->drawTags(p, std::ranges::subrange(impl->tags.cbegin(), middle));
|
||||
|
||||
if (impl->cursorVisible()) {
|
||||
impl->drawEditor(p, palette(), impl->offset());
|
||||
} else if (!impl->editorText().isEmpty()) {
|
||||
impl->drawTags(p, std::ranges::subrange(middle, middle + 1));
|
||||
}
|
||||
|
||||
// tags
|
||||
impl->drawTags(p, std::ranges::subrange(middle + 1, impl->tags.cend()));
|
||||
}
|
||||
|
||||
void TagsEdit::timerEvent(QTimerEvent* event) {
|
||||
if (event->timerId() == impl->blink_timer) {
|
||||
impl->blink_status = !impl->blink_status;
|
||||
viewport()->update();
|
||||
}
|
||||
}
|
||||
|
||||
void TagsEdit::mousePressEvent(QMouseEvent* event) {
|
||||
// we don't want to change cursor position if this event is part of focusIn
|
||||
using namespace std::chrono_literals;
|
||||
if (impl->restore_cursor_position_on_focus_click && elapsed(impl->focused_at) < 1ms) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool keep_cursor_visible = true;
|
||||
EVERLOAD_TAGS_SCOPE_EXIT {
|
||||
impl->update1(keep_cursor_visible);
|
||||
};
|
||||
|
||||
switch (impl->handleClick(event, impl->offset())) {
|
||||
using enum Impl::HandleClickResult;
|
||||
case removed_a_tag:
|
||||
keep_cursor_visible = false;
|
||||
return;
|
||||
case moved_cursor_in_a_tag:
|
||||
// keep cursor visible because the cursor can be at the border, we need to enlarge the space around it;
|
||||
case moved_cursor_across_tags:
|
||||
// keep the cursor visible because it can move out of the view
|
||||
return;
|
||||
case unhandled:
|
||||
break;
|
||||
}
|
||||
|
||||
// add new tag closest to the cursor
|
||||
for (auto it = begin(impl->tags); it != end(impl->tags); ++it) {
|
||||
// find the row
|
||||
if (it->rect.translated(-impl->offset()).bottom() < event->pos().y()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// find the closest spot
|
||||
auto const row = it->rect.translated(-impl->offset()).top();
|
||||
while (it != end(impl->tags) && it->rect.translated(-impl->offset()).top() == row &&
|
||||
event->pos().x() > it->rect.translated(-impl->offset()).left()) {
|
||||
++it;
|
||||
}
|
||||
|
||||
impl->editNewTag(static_cast<size_t>(std::distance(begin(impl->tags), it)));
|
||||
return;
|
||||
}
|
||||
|
||||
// append a new nag
|
||||
impl->editNewTag(impl->tags.size());
|
||||
}
|
||||
|
||||
QSize TagsEdit::sizeHint() const {
|
||||
return minimumSizeHint();
|
||||
}
|
||||
|
||||
QSize TagsEdit::minimumSizeHint() const {
|
||||
ensurePolished();
|
||||
QFontMetrics fm = fontMetrics();
|
||||
QRect rect(0, 0, impl->pillWidth(fm.maxWidth(), true), impl->pillHeight(fm.height()));
|
||||
rect += contentsMargins() + viewport()->contentsMargins() + viewportMargins();
|
||||
return rect.size();
|
||||
}
|
||||
|
||||
int TagsEdit::heightForWidth(int w) const {
|
||||
auto const 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(contents_rect);
|
||||
contents_rect += contentsMargins() + viewport()->contentsMargins() + viewportMargins();
|
||||
return contents_rect.height();
|
||||
}
|
||||
|
||||
void TagsEdit::keyPressEvent(QKeyEvent* event) {
|
||||
if (impl->read_only) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == QKeySequence::SelectAll) {
|
||||
impl->selectAll();
|
||||
} else if (event == QKeySequence::SelectPreviousChar) {
|
||||
impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), true);
|
||||
} else if (event == QKeySequence::SelectNextChar) {
|
||||
impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), true);
|
||||
} else {
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Left:
|
||||
if (event->modifiers() == Qt::ControlModifier || impl->cursor == 0) {
|
||||
impl->editPreviousTag();
|
||||
} else {
|
||||
impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
if (event->modifiers() == Qt::ControlModifier || impl->cursor == impl->editorText().size()) {
|
||||
impl->editNextTag();
|
||||
} else {
|
||||
impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Up: {
|
||||
auto const before = impl->tags | std::views::take(impl->editing_index);
|
||||
auto const it = std::ranges::find_if(before, [&](auto const& tag) {
|
||||
return impl->editorRect()
|
||||
.translated(0, -impl->tag_v_spacing - fontMetrics().height())
|
||||
.intersects(tag.rect);
|
||||
});
|
||||
if (it != end(before)) {
|
||||
impl->editTag(std::distance(begin(before), it));
|
||||
}
|
||||
} break;
|
||||
case Qt::Key_Down: {
|
||||
auto const after = impl->tags | std::views::drop(impl->editing_index + 1);
|
||||
auto const it = std::ranges::find_if(after, [&](auto const& tag) {
|
||||
return impl->editorRect()
|
||||
.translated(0, impl->tag_v_spacing + fontMetrics().height())
|
||||
.intersects(tag.rect);
|
||||
});
|
||||
if (it != end(after)) {
|
||||
impl->editTag(impl->editing_index + 1 + std::distance(begin(after), it));
|
||||
}
|
||||
} break;
|
||||
case Qt::Key_Home:
|
||||
if (event->modifiers() == Qt::ControlModifier) {
|
||||
impl->editTag(0);
|
||||
} else {
|
||||
impl->moveCursor(0, false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_End:
|
||||
if (event->modifiers() == Qt::ControlModifier) {
|
||||
impl->editTag(impl->tags.size() - 1);
|
||||
} else {
|
||||
impl->moveCursor(impl->editorText().length(), false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Backspace:
|
||||
if (!impl->editorText().isEmpty()) {
|
||||
impl->removeBackwardOne();
|
||||
} else if (impl->editing_index > 0) {
|
||||
impl->editPreviousTag();
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Space:
|
||||
if (!impl->editorText().isEmpty()) {
|
||||
impl->editNewTag(impl->editing_index + 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (isAcceptableInput(*event)) {
|
||||
if (impl->hasSelection()) {
|
||||
impl->removeSelection();
|
||||
}
|
||||
impl->editorText().insert(impl->cursor, event->text());
|
||||
impl->cursor = impl->cursor + event->text().length();
|
||||
break;
|
||||
} else {
|
||||
event->setAccepted(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl->update1();
|
||||
|
||||
impl->completer->setCompletionPrefix(impl->editorText());
|
||||
impl->completer->complete();
|
||||
|
||||
emit tagsEdited();
|
||||
}
|
||||
|
||||
void TagsEdit::completion(std::vector<QString> const& completions) {
|
||||
impl->completer = std::make_unique<QCompleter>([&] {
|
||||
QStringList ret;
|
||||
std::copy(completions.begin(), completions.end(), std::back_inserter(ret));
|
||||
return ret;
|
||||
}());
|
||||
impl->setupCompleter();
|
||||
}
|
||||
|
||||
void TagsEdit::completion(QStringList const& completions) {
|
||||
impl->completer = std::make_unique<QCompleter>(completions);
|
||||
impl->setupCompleter();
|
||||
}
|
||||
|
||||
void TagsEdit::tags(std::vector<QString> const& tags) {
|
||||
impl->setTags(tags);
|
||||
impl->update1();
|
||||
}
|
||||
|
||||
void TagsEdit::tags(QStringList const& tags) {
|
||||
impl->setTags(tags);
|
||||
impl->update1();
|
||||
}
|
||||
|
||||
std::vector<QString> TagsEdit::tags() const {
|
||||
std::vector<QString> ret;
|
||||
impl->getTags(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
QStringList TagsEdit::tags2() const {
|
||||
QStringList ret;
|
||||
impl->getTags(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void TagsEdit::mouseMoveEvent(QMouseEvent* event) {
|
||||
for (size_t i = 0; i < impl->tags.size(); ++i) {
|
||||
if (impl->inCrossArea(i, event->pos(), impl->offset())) {
|
||||
viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (impl->contentsRect().contains(event->pos())) {
|
||||
viewport()->setCursor(Qt::IBeamCursor);
|
||||
} else {
|
||||
QAbstractScrollArea::mouseMoveEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void TagsEdit::config(Config config) {
|
||||
if (impl->unique && impl->unique != config.behavior.unique) {
|
||||
impl->removeDuplicates();
|
||||
}
|
||||
static_cast<StyleConfig&>(*impl) = config.style;
|
||||
static_cast<BehaviorConfig&>(*impl) = config.behavior;
|
||||
impl->update1();
|
||||
}
|
||||
|
||||
Config TagsEdit::config() const {
|
||||
return Config{
|
||||
.style = static_cast<StyleConfig const&>(*impl),
|
||||
.behavior = static_cast<BehaviorConfig const&>(*impl),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace everload_tags
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Nicolai Trandafil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "tags_line_edit.hpp"
|
||||
|
||||
#include "common.hpp"
|
||||
#include "scope_exit.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCompleter>
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QStyle>
|
||||
#include <QStyleHints>
|
||||
#include <QStyleOptionFrame>
|
||||
#include <QTextLayout>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
inline constexpr QMargins magic_margins = {2, 2, 2, 3};
|
||||
|
||||
struct TagsLineEdit::Impl : Common {
|
||||
explicit Impl(TagsLineEdit* const& ifce, Config config)
|
||||
: Common{{config.style}, {config.behavior}, {}}, ifce{ifce} {}
|
||||
|
||||
QPoint offset() const {
|
||||
return {hscroll, 0};
|
||||
}
|
||||
|
||||
using Common::drawTags;
|
||||
|
||||
template <std::ranges::input_range Range>
|
||||
void drawTags(QPainter& p, Range range) const {
|
||||
drawTags(p, range, ifce->fontMetrics(), -offset(),
|
||||
!read_only && (!restore_cursor_position_on_focus_click || ifce->hasFocus()));
|
||||
}
|
||||
|
||||
QRect contentsRect() const {
|
||||
return ifce->contentsRect() - magic_margins;
|
||||
}
|
||||
|
||||
using Common::calcRects;
|
||||
|
||||
void calcRects() {
|
||||
auto const r = contentsRect();
|
||||
auto lt = r.topLeft();
|
||||
|
||||
auto const middle = tags.begin() + static_cast<ptrdiff_t>(editing_index);
|
||||
|
||||
calcRects(lt, std::ranges::subrange(tags.begin(), middle), ifce->fontMetrics(), std::nullopt, !read_only);
|
||||
|
||||
if (cursorVisible() || !editorText().isEmpty()) {
|
||||
calcRects(lt, std::ranges::subrange(middle, middle + 1), ifce->fontMetrics(), std::nullopt, !read_only);
|
||||
}
|
||||
|
||||
calcRects(lt, std::ranges::subrange(middle + 1, tags.end()), ifce->fontMetrics(), std::nullopt, !read_only);
|
||||
}
|
||||
|
||||
void setEditorText(QString const& text) {
|
||||
tags[editing_index].text = text;
|
||||
moveCursor(editorText().length(), false);
|
||||
update1();
|
||||
}
|
||||
|
||||
void setupCompleter() {
|
||||
completer->setWidget(ifce);
|
||||
connect(completer.get(), static_cast<void (QCompleter::*)(QString const&)>(&QCompleter::activated), ifce,
|
||||
[this](QString const& text) { setEditorText(text); });
|
||||
}
|
||||
|
||||
int pillsWidth() const {
|
||||
if (tags.size() == 1 && tags.front().text.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int left = tags.front().rect.left();
|
||||
int right = tags.back().rect.right();
|
||||
|
||||
if (editing_index == 0 && !(cursorVisible() || !editorText().isEmpty())) {
|
||||
left = tags[1].rect.left();
|
||||
} else if (editing_index == tags.size() - 1 && !(cursorVisible() || !editorText().isEmpty())) {
|
||||
right = tags[tags.size() - 2].rect.right();
|
||||
}
|
||||
|
||||
return right - left + 1;
|
||||
}
|
||||
|
||||
void updateHScrollRange() {
|
||||
auto const contents_rect = contentsRect();
|
||||
auto const width_used = pillsWidth();
|
||||
|
||||
if (contents_rect.width() < width_used) {
|
||||
hscroll_max = width_used - contents_rect.width();
|
||||
} else {
|
||||
hscroll_max = 0;
|
||||
}
|
||||
|
||||
hscroll = std::clamp(hscroll, hscroll_min, hscroll_max);
|
||||
}
|
||||
|
||||
// scroll to the cursor
|
||||
void ensureCursorIsVisible() {
|
||||
auto const contents_rect = contentsRect().translated(offset());
|
||||
int const cursor_x = (editorRect() - pill_thickness).left() + qRound(cursorToX());
|
||||
|
||||
if (contents_rect.right() < cursor_x) {
|
||||
hscroll = cursor_x - contents_rect.width();
|
||||
} else if (cursor_x < contents_rect.left()) {
|
||||
hscroll = cursor_x - 1;
|
||||
}
|
||||
|
||||
hscroll = std::clamp(hscroll, hscroll_min, hscroll_max);
|
||||
}
|
||||
|
||||
// scrolls to the cursor if `keep_cursor_visible`
|
||||
void update1(bool keep_cursor_visible = true) {
|
||||
updateDisplayText();
|
||||
calcRects();
|
||||
updateHScrollRange();
|
||||
if (keep_cursor_visible) {
|
||||
ensureCursorIsVisible();
|
||||
}
|
||||
updateCursorBlinking(ifce);
|
||||
ifce->update();
|
||||
}
|
||||
|
||||
void initStyleOption(QStyleOptionFrame* option) const {
|
||||
assert(option);
|
||||
option->initFrom(ifce);
|
||||
option->rect = ifce->contentsRect();
|
||||
option->lineWidth = 1;
|
||||
option->midLineWidth = 0;
|
||||
option->state |= QStyle::State_Sunken;
|
||||
if (ifce->underMouse()) {
|
||||
option->state |= QStyle::State_MouseOver;
|
||||
}
|
||||
option->features = QStyleOptionFrame::None;
|
||||
}
|
||||
|
||||
TagsLineEdit* const ifce;
|
||||
|
||||
int const hscroll_min = 0;
|
||||
int hscroll = 0;
|
||||
int hscroll_max = 0;
|
||||
};
|
||||
|
||||
TagsLineEdit::TagsLineEdit(QWidget* parent, Config config)
|
||||
: QWidget(parent), impl(std::make_unique<Impl>(this, config)) {
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setCursor(Qt::IBeamCursor);
|
||||
setAttribute(Qt::WA_InputMethodEnabled, true);
|
||||
setAttribute(Qt::WA_Hover, true);
|
||||
setMouseTracking(true);
|
||||
|
||||
impl->setupCompleter();
|
||||
impl->setCursorVisible(hasFocus(), this);
|
||||
impl->updateDisplayText();
|
||||
}
|
||||
|
||||
TagsLineEdit::~TagsLineEdit() = default;
|
||||
|
||||
void TagsLineEdit::resizeEvent(QResizeEvent*) {
|
||||
impl->calcRects();
|
||||
}
|
||||
|
||||
void TagsLineEdit::focusInEvent(QFocusEvent* e) {
|
||||
QWidget::focusInEvent(e);
|
||||
impl->focused_at = std::chrono::steady_clock::now();
|
||||
impl->setCursorVisible(true, this);
|
||||
impl->updateDisplayText();
|
||||
impl->calcRects();
|
||||
impl->updateHScrollRange();
|
||||
if (e->reason() != Qt::FocusReason::MouseFocusReason || impl->restore_cursor_position_on_focus_click) {
|
||||
impl->ensureCursorIsVisible();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void TagsLineEdit::focusOutEvent(QFocusEvent* e) {
|
||||
QWidget::focusOutEvent(e);
|
||||
impl->setCursorVisible(false, this);
|
||||
impl->updateDisplayText();
|
||||
impl->calcRects();
|
||||
impl->updateHScrollRange();
|
||||
update();
|
||||
}
|
||||
|
||||
void TagsLineEdit::paintEvent(QPaintEvent* e) {
|
||||
QWidget::paintEvent(e);
|
||||
|
||||
QPainter p(this);
|
||||
|
||||
// opt
|
||||
auto const panel = [this] {
|
||||
QStyleOptionFrame panel;
|
||||
impl->initStyleOption(&panel);
|
||||
return panel;
|
||||
}();
|
||||
|
||||
// draw frame
|
||||
style()->drawPrimitive(QStyle::PE_PanelLineEdit, &panel, &p, this);
|
||||
|
||||
// clip
|
||||
auto const rect = impl->contentsRect();
|
||||
p.setClipRect(rect);
|
||||
|
||||
auto const middle = impl->tags.cbegin() + static_cast<ptrdiff_t>(impl->editing_index);
|
||||
|
||||
// tags
|
||||
impl->drawTags(p, std::ranges::subrange(impl->tags.cbegin(), middle));
|
||||
|
||||
if (impl->cursorVisible()) {
|
||||
impl->drawEditor(p, palette(), impl->offset());
|
||||
} else if (!impl->editorText().isEmpty()) {
|
||||
impl->drawTags(p, std::ranges::subrange(middle, middle + 1));
|
||||
}
|
||||
|
||||
// tags
|
||||
impl->drawTags(p, std::ranges::subrange(middle + 1, impl->tags.cend()));
|
||||
}
|
||||
|
||||
void TagsLineEdit::timerEvent(QTimerEvent* event) {
|
||||
if (event->timerId() == impl->blink_timer) {
|
||||
impl->blink_status = !impl->blink_status;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void TagsLineEdit::mousePressEvent(QMouseEvent* event) {
|
||||
// we don't want to change cursor position if this event is part of focusIn
|
||||
using namespace std::chrono_literals;
|
||||
if (impl->read_only || (impl->restore_cursor_position_on_focus_click && elapsed(impl->focused_at) < 1ms)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool keep_cursor_visible = true;
|
||||
EVERLOAD_TAGS_SCOPE_EXIT {
|
||||
impl->update1(keep_cursor_visible);
|
||||
};
|
||||
|
||||
switch (impl->handleClick(event, impl->offset())) {
|
||||
using enum Impl::HandleClickResult;
|
||||
case removed_a_tag:
|
||||
keep_cursor_visible = false;
|
||||
return;
|
||||
case moved_cursor_in_a_tag:
|
||||
// keep cursor visible because the cursor can be at the border, we need to enlarge the space around it;
|
||||
case moved_cursor_across_tags:
|
||||
// keep the cursor visible because it can move out of the view
|
||||
return;
|
||||
case unhandled:
|
||||
break;
|
||||
}
|
||||
|
||||
// add new tag closed to the cursor
|
||||
for (auto it = begin(impl->tags); it != end(impl->tags); ++it) {
|
||||
// find the closest spot
|
||||
if (event->pos().x() > it->rect.translated(-impl->offset()).left()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
impl->editNewTag(static_cast<size_t>(std::distance(begin(impl->tags), it)));
|
||||
return;
|
||||
}
|
||||
|
||||
// append a new nag
|
||||
impl->editNewTag(impl->tags.size());
|
||||
}
|
||||
|
||||
QSize TagsLineEdit::sizeHint() const {
|
||||
ensurePolished();
|
||||
|
||||
auto const fm = fontMetrics();
|
||||
QRect rect(0, 0, impl->pillWidth(fm.boundingRect(QLatin1Char('x')).width() * 17, true),
|
||||
impl->pillHeight(fm.height()));
|
||||
rect += magic_margins;
|
||||
|
||||
QStyleOptionFrame opt;
|
||||
impl->initStyleOption(&opt);
|
||||
|
||||
return (style()->sizeFromContents(QStyle::CT_LineEdit, &opt, rect.size(), this));
|
||||
}
|
||||
|
||||
QSize TagsLineEdit::minimumSizeHint() const {
|
||||
ensurePolished();
|
||||
|
||||
auto const fm = fontMetrics();
|
||||
QRect rect(0, 0, impl->pillWidth(fm.maxWidth(), true), impl->pillHeight(fm.height()));
|
||||
rect += magic_margins;
|
||||
|
||||
QStyleOptionFrame opt;
|
||||
impl->initStyleOption(&opt);
|
||||
|
||||
return (style()->sizeFromContents(QStyle::CT_LineEdit, &opt, rect.size(), this));
|
||||
}
|
||||
|
||||
void TagsLineEdit::keyPressEvent(QKeyEvent* event) {
|
||||
if (impl->read_only) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == QKeySequence::SelectAll) {
|
||||
impl->selectAll();
|
||||
} else if (event == QKeySequence::SelectPreviousChar) {
|
||||
impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), true);
|
||||
} else if (event == QKeySequence::SelectNextChar) {
|
||||
impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), true);
|
||||
} else {
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Left:
|
||||
if (event->modifiers() == Qt::ControlModifier || impl->cursor == 0) {
|
||||
impl->editPreviousTag();
|
||||
} else {
|
||||
impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
if (event->modifiers() == Qt::ControlModifier || impl->cursor == impl->editorText().size()) {
|
||||
impl->editNextTag();
|
||||
} else {
|
||||
impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Home:
|
||||
if (event->modifiers() == Qt::ControlModifier) {
|
||||
impl->editTag(0);
|
||||
} else {
|
||||
impl->moveCursor(0, false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_End:
|
||||
if (event->modifiers() == Qt::ControlModifier) {
|
||||
impl->editTag(impl->tags.size() - 1);
|
||||
} else {
|
||||
impl->moveCursor(impl->editorText().length(), false);
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Backspace:
|
||||
if (!impl->editorText().isEmpty()) {
|
||||
impl->removeBackwardOne();
|
||||
} else if (impl->editing_index > 0) {
|
||||
impl->editPreviousTag();
|
||||
}
|
||||
break;
|
||||
case Qt::Key_Space:
|
||||
if (!impl->editorText().isEmpty()) {
|
||||
impl->editNewTag(impl->editing_index + 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (isAcceptableInput(*event)) {
|
||||
if (impl->hasSelection()) {
|
||||
impl->removeSelection();
|
||||
}
|
||||
impl->tags[impl->editing_index].text.insert(impl->cursor, event->text());
|
||||
impl->cursor += event->text().length();
|
||||
break;
|
||||
} else {
|
||||
event->setAccepted(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl->update1();
|
||||
|
||||
impl->completer->setCompletionPrefix(impl->editorText());
|
||||
impl->completer->complete();
|
||||
|
||||
emit tagsEdited();
|
||||
}
|
||||
|
||||
void TagsLineEdit::completion(std::vector<QString> const& completions) {
|
||||
QStringList tmp;
|
||||
std::copy(begin(completions), end(completions), std::back_inserter(tmp));
|
||||
impl->completer = std::make_unique<QCompleter>(std::move(tmp));
|
||||
impl->setupCompleter();
|
||||
}
|
||||
|
||||
void TagsLineEdit::completion(QStringList const& completions) {
|
||||
impl->completer = std::make_unique<QCompleter>(completions);
|
||||
impl->setupCompleter();
|
||||
}
|
||||
|
||||
void TagsLineEdit::tags(std::vector<QString> const& tags) {
|
||||
impl->setTags(tags);
|
||||
impl->update1();
|
||||
}
|
||||
|
||||
void TagsLineEdit::tags(QStringList const& tags) {
|
||||
impl->setTags(tags);
|
||||
impl->update1();
|
||||
}
|
||||
|
||||
std::vector<QString> TagsLineEdit::tags() const {
|
||||
std::vector<QString> ret;
|
||||
impl->getTags(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
QStringList TagsLineEdit::tags2() const {
|
||||
QStringList ret;
|
||||
impl->getTags(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void TagsLineEdit::mouseMoveEvent(QMouseEvent* event) {
|
||||
event->accept();
|
||||
for (size_t i = 0; i < impl->tags.size(); ++i) {
|
||||
if (impl->inCrossArea(i, event->pos(), impl->offset())) {
|
||||
setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCursor(Qt::IBeamCursor);
|
||||
}
|
||||
|
||||
void TagsLineEdit::wheelEvent(QWheelEvent* event) {
|
||||
event->accept();
|
||||
impl->calcRects();
|
||||
impl->updateHScrollRange();
|
||||
impl->hscroll = std::clamp(impl->hscroll - event->pixelDelta().x(), impl->hscroll_min, impl->hscroll_max);
|
||||
update();
|
||||
}
|
||||
|
||||
void TagsLineEdit::config(Config config) {
|
||||
if (impl->unique && impl->unique != config.behavior.unique) {
|
||||
impl->removeDuplicates();
|
||||
}
|
||||
static_cast<StyleConfig&>(*impl) = config.style;
|
||||
static_cast<BehaviorConfig&>(*impl) = config.behavior;
|
||||
impl->update1();
|
||||
}
|
||||
|
||||
Config TagsLineEdit::config() const {
|
||||
return Config{
|
||||
.style = static_cast<StyleConfig const&>(*impl),
|
||||
.behavior = static_cast<BehaviorConfig const&>(*impl),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace everload_tags
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
|
||||
#include "../include/config.hpp"
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace everload_tags {
|
||||
|
||||
inline void removeDuplicates(std::vector<Tag>& tags) {
|
||||
std::unordered_map<QString, size_t> unique;
|
||||
for (auto const i : std::views::iota(size_t{0}, tags.size())) {
|
||||
unique.emplace(tags[i].text, i);
|
||||
}
|
||||
|
||||
for (auto b = tags.rbegin(), it = b, e = tags.rend(); it != e;) {
|
||||
if (auto const i = static_cast<size_t>(std::distance(it, e) - 1); unique.at(it->text) != i) {
|
||||
tags.erase(it++.base() - 1);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace everload_tags
|
||||
Reference in New Issue
Block a user