11#include <QAbstractItemView>
14#include <QContextMenuEvent>
34 , Completer_ { *new QCompleter { &model, this } }
37 Completer_.setCompletionRole (Qt::DisplayRole);
38 Completer_.setCaseSensitivity (Qt::CaseInsensitive);
39 Completer_.setCompletionMode (QCompleter::PopupCompletion);
40 Completer_.setWidget (&Edit_);
43 qOverload<const QString&> (&QCompleter::activated),
45 &TagsEditorAddon::InsertTag);
47 &QLineEdit::textEdited,
49 &TagsEditorAddon::Complete);
51 &QLineEdit::cursorPositionChanged,
55 if (Completer_.popup ()->isVisible () && PrefixUnderCursor () != CompletionPrefix_)
59 &QLineEdit::textChanged,
64 Selector_->SetSelections (
GetTags ());
67 &QLineEdit::editingFinished,
69 &TagsEditorAddon::ReportEdit);
73 &TagsEditorAddon::ReportEdit);
91 if (separator.isEmpty ())
92 throw std::invalid_argument {
"TagsEditorAddon: the tags separator must not be empty" };
94 Separator_ = separator;
102 const auto action =
new QAction { QIcon::fromTheme (QStringLiteral (
"mail-tagged")), tr (
"Select tags..."), popup };
103 action->setShortcut (Qt::ALT | Qt::Key_Down);
104 action->setShortcutContext (Qt::WidgetShortcut);
110 popup->move (QCursor::pos ());
113 Edit_.addAction (action);
114 Edit_.installEventFilter (
this);
115 SelectorAction_ = action;
120 const auto button =
new QToolButton { &Edit_ };
121 button->setDefaultAction (action);
122 button->setIconSize ({ 16, 16 });
123 button->setCursor (Qt::ArrowCursor);
124 button->setStyleSheet (QStringLiteral (
"QToolButton { border: none; padding: 0px; }"));
125 manager->
Add (button);
131 throw std::logic_error {
"TagsEditorAddon: a selector is already bound" };
133 Selector_ = &selector;
134 selector.
SetModel (*Completer_.model ());
140 &TagsEditorAddon::ApplyChoice);
147 Selector_->SetSelections (tags);
148 Edit_.setText (tags.join (Separator_));
153 if (IsEdited () ||
TagsChange { .Before_ = ShownTags_, .After_ = tags }.
IsEmpty ())
161 return SplitTags (Edit_.text (), Separator_);
168 if (event->type () != QEvent::ContextMenu || !SelectorAction_)
171 ShowContextMenu (
static_cast<QContextMenuEvent*
> (event)->globalPos ());
175 if (event->type () != QEvent::KeyPress || obj != Completer_.popup ())
178 const auto popup = Completer_.popup ();
179 switch (
static_cast<QKeyEvent*
> (event)->key ())
185 const auto& current = popup->currentIndex ();
187 if (current.isValid ())
188 InsertTag (current.data (Completer_.completionRole ()).toString ());
191 case Qt::Key_Backtab:
199 void TagsEditorAddon::ShowContextMenu (
const QPoint& pos)
201 const auto menu = Edit_.createStandardContextMenu ();
202 menu->setAttribute (Qt::WA_DeleteOnClose);
204 const auto first = menu->actions ().value (0);
205 menu->insertAction (first, SelectorAction_);
206 menu->insertSeparator (first);
210 TagsEditorAddon::TokenRange TagsEditorAddon::TokenUnderCursor ()
const
212 const auto& text = Edit_.text ();
213 const auto cursor = Edit_.cursorPosition ();
217 auto end = text.size ();
218 for (
auto pos = text.indexOf (delimiter); pos != -1; pos = text.indexOf (delimiter, pos + delimiter.size ()))
225 begin = pos + delimiter.size ();
229 while (begin < end && text.at (begin).isSpace ())
232 return { .Begin_ = begin, .End_ = end };
235 QString TagsEditorAddon::PrefixUnderCursor ()
const
237 const auto [begin, end] = TokenUnderCursor ();
238 return Edit_.text ().mid (begin, end - begin).trimmed ();
241 void TagsEditorAddon::Complete ()
243 CompletionPrefix_ = PrefixUnderCursor ();
244 Completer_.setCompletionPrefix (CompletionPrefix_);
248 const auto popup = Completer_.popup ();
249 popup->installEventFilter (
this);
250 popup->setCurrentIndex (Completer_.completionModel ()->index (0, 0));
251 Completer_.complete ();
254 void TagsEditorAddon::InsertTag (
const QString& completion)
256 const auto [begin, end] = TokenUnderCursor ();
257 auto text = Edit_.text ();
258 text.replace (begin, end - begin, completion);
259 Edit_.setText (text);
260 Edit_.setCursorPosition (
static_cast<int> (begin + completion.size ()));
265 void TagsEditorAddon::ApplyChoice (
const QStringList& chosen)
267 const auto& model = *Completer_.model ();
269 for (
int row = 0; row < model.rowCount (); ++row)
270 known << model.index (row, 0).data ().toString ();
273 for (
const auto& tag :
GetTags ())
274 if (!known.contains (tag) && !tags.contains (tag))
277 Edit_.setText (tags.join (Separator_));
281 bool TagsEditorAddon::IsEdited ()
const
283 return !TagsChange { .Before_ = ShownTags_, .After_ =
GetTags () }.IsEmpty ();
286 void TagsEditorAddon::ReportEdit ()
288 const TagsChange change { .Before_ = ShownTags_, .After_ =
GetTags () };
289 if (change.IsEmpty ())
292 ShownTags_ = change.After_;
A checklist over a model of names.
void selectionEdited(const QStringList &selected)
Emitted after the user has changed the selection, with GetSelections().
void SetModel(QAbstractItemModel &model)
Lists the names of model, which is not owned.
void SetSelections(const QStringList &names)
Replaces the selection.
A CategorySelector in a window of its own, kept within the screen.
QStringList SplitTags(QStringView text, const QString &separator)
Splits a string with tags into the tags.
QString GetDefaultTagsSeparator()
QString GetTagsDelimiter(const QString &separator)
Returns the delimiter the tags are split by for the given separator they are joined with.