LeechCraft 0.6.70-19424-g027e980fd1
Modular cross-platform feature rich live environment.
Loading...
Searching...
No Matches
tagseditoraddon.cpp
Go to the documentation of this file.
1/**********************************************************************
2 * LeechCraft - modular cross-platform feature rich internet client.
3 * Copyright (C) 2006-2014 Georg Rudoy
4 *
5 * Distributed under the Boost Software License, Version 1.0.
6 * (See accompanying file LICENSE or copy at https://www.boost.org/LICENSE_1_0.txt)
7 **********************************************************************/
8
9#include "tagseditoraddon.h"
10#include <stdexcept>
11#include <QAbstractItemView>
12#include <QAction>
13#include <QCompleter>
14#include <QContextMenuEvent>
15#include <QCursor>
16#include <QIcon>
17#include <QKeyEvent>
18#include <QLineEdit>
19#include <QMenu>
20#include <QSet>
21#include <QToolButton>
24#include "categoryselector.h"
26#include "tagschange.h"
27#include "util.h"
28
29namespace LC::Util
30{
31 TagsEditorAddon::TagsEditorAddon (QLineEdit& edit, QAbstractItemModel& model, Flags flags)
32 : QObject { &edit }
33 , Edit_ { edit }
34 , Completer_ { *new QCompleter { &model, this } }
35 , Separator_ { GetDefaultTagsSeparator () }
36 {
37 Completer_.setCompletionRole (Qt::DisplayRole);
38 Completer_.setCaseSensitivity (Qt::CaseInsensitive);
39 Completer_.setCompletionMode (QCompleter::PopupCompletion);
40 Completer_.setWidget (&Edit_);
41
42 connect (&Completer_,
43 qOverload<const QString&> (&QCompleter::activated),
44 this,
45 &TagsEditorAddon::InsertTag);
46 connect (&Edit_,
47 &QLineEdit::textEdited,
48 this,
49 &TagsEditorAddon::Complete);
50 connect (&Edit_,
51 &QLineEdit::cursorPositionChanged,
52 this,
53 [this]
54 {
55 if (Completer_.popup ()->isVisible () && PrefixUnderCursor () != CompletionPrefix_)
56 Complete ();
57 });
58 connect (&Edit_,
59 &QLineEdit::textChanged,
60 this,
61 [this]
62 {
63 if (Selector_)
64 Selector_->SetSelections (GetTags ());
65 });
66 connect (&Edit_,
67 &QLineEdit::editingFinished,
68 this,
69 &TagsEditorAddon::ReportEdit);
70 connect (this,
72 this,
73 &TagsEditorAddon::ReportEdit);
74
75 if (!(flags & Flag::NoSelectorButton))
77 }
78
79 TagsEditorAddon::TagsEditorAddon (QLineEdit& edit, ITagsManager& tm, Flags flags)
80 : TagsEditorAddon { edit, *tm.GetModel (), flags }
81 {
82 }
83
85 {
86 return Separator_;
87 }
88
89 void TagsEditorAddon::SetSeparator (const QString& separator)
90 {
91 if (separator.isEmpty ())
92 throw std::invalid_argument { "TagsEditorAddon: the tags separator must not be empty" };
93
94 Separator_ = separator;
95 }
96
98 {
99 const auto popup = new CategorySelectorWindow { CategorySelectorWindow::Buttons::Close, &Edit_ };
100 BindSelector (popup->Selector ());
101
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);
105 connect (action,
106 &QAction::triggered,
107 popup,
108 [popup]
109 {
110 popup->move (QCursor::pos ());
111 popup->show ();
112 });
113 Edit_.addAction (action);
114 Edit_.installEventFilter (this);
115 SelectorAction_ = action;
116
117 if (!manager)
118 manager = new LineEditButtonManager { &Edit_ };
119
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);
126 }
127
129 {
130 if (Selector_)
131 throw std::logic_error { "TagsEditorAddon: a selector is already bound" };
132
133 Selector_ = &selector;
134 selector.SetModel (*Completer_.model ());
135 selector.SetSelections (GetTags ());
136
137 connect (&selector,
139 this,
140 &TagsEditorAddon::ApplyChoice);
141 }
142
143 void TagsEditorAddon::SetTags (const QStringList& tags)
144 {
145 ShownTags_ = tags;
146 if (Selector_)
147 Selector_->SetSelections (tags);
148 Edit_.setText (tags.join (Separator_));
149 }
150
151 void TagsEditorAddon::UpdateTags (const QStringList& tags)
152 {
153 if (IsEdited () || TagsChange { .Before_ = ShownTags_, .After_ = tags }.IsEmpty ())
154 return;
155
156 SetTags (tags);
157 }
158
159 QStringList TagsEditorAddon::GetTags () const
160 {
161 return SplitTags (Edit_.text (), Separator_);
162 }
163
164 bool TagsEditorAddon::eventFilter (QObject *obj, QEvent *event)
165 {
166 if (obj == &Edit_)
167 {
168 if (event->type () != QEvent::ContextMenu || !SelectorAction_)
169 return false;
170
171 ShowContextMenu (static_cast<QContextMenuEvent*> (event)->globalPos ());
172 return true;
173 }
174
175 if (event->type () != QEvent::KeyPress || obj != Completer_.popup ())
176 return false;
177
178 const auto popup = Completer_.popup ();
179 switch (static_cast<QKeyEvent*> (event)->key ())
180 {
181 case Qt::Key_Return:
182 case Qt::Key_Enter:
183 case Qt::Key_Tab:
184 {
185 const auto& current = popup->currentIndex ();
186 popup->hide ();
187 if (current.isValid ())
188 InsertTag (current.data (Completer_.completionRole ()).toString ());
189 return true;
190 }
191 case Qt::Key_Backtab:
192 popup->hide ();
193 return true;
194 default:
195 return false;
196 }
197 }
198
199 void TagsEditorAddon::ShowContextMenu (const QPoint& pos)
200 {
201 const auto menu = Edit_.createStandardContextMenu ();
202 menu->setAttribute (Qt::WA_DeleteOnClose);
203
204 const auto first = menu->actions ().value (0);
205 menu->insertAction (first, SelectorAction_);
206 menu->insertSeparator (first);
207 menu->popup (pos);
208 }
209
210 TagsEditorAddon::TokenRange TagsEditorAddon::TokenUnderCursor () const
211 {
212 const auto& text = Edit_.text ();
213 const auto cursor = Edit_.cursorPosition ();
214 const auto& delimiter = GetTagsDelimiter (Separator_);
215
216 qsizetype begin = 0;
217 auto end = text.size ();
218 for (auto pos = text.indexOf (delimiter); pos != -1; pos = text.indexOf (delimiter, pos + delimiter.size ()))
219 {
220 if (pos >= cursor)
221 {
222 end = pos;
223 break;
224 }
225 begin = pos + delimiter.size ();
226 }
227
228 // The whitespace after a separator belongs to it, the whitespace before the next one to the token.
229 while (begin < end && text.at (begin).isSpace ())
230 ++begin;
231
232 return { .Begin_ = begin, .End_ = end };
233 }
234
235 QString TagsEditorAddon::PrefixUnderCursor () const
236 {
237 const auto [begin, end] = TokenUnderCursor ();
238 return Edit_.text ().mid (begin, end - begin).trimmed ();
239 }
240
241 void TagsEditorAddon::Complete ()
242 {
243 CompletionPrefix_ = PrefixUnderCursor ();
244 Completer_.setCompletionPrefix (CompletionPrefix_);
245
246 // The completer hands the keys to the edit by a direct event() call, so the edit's
247 // filters never see them; filtering the popup instead keeps Enter from finishing the edit.
248 const auto popup = Completer_.popup ();
249 popup->installEventFilter (this);
250 popup->setCurrentIndex (Completer_.completionModel ()->index (0, 0));
251 Completer_.complete ();
252 }
253
254 void TagsEditorAddon::InsertTag (const QString& completion)
255 {
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 ()));
261
262 emit tagPicked ();
263 }
264
265 void TagsEditorAddon::ApplyChoice (const QStringList& chosen)
266 {
267 const auto& model = *Completer_.model ();
268 QSet<QString> known;
269 for (int row = 0; row < model.rowCount (); ++row)
270 known << model.index (row, 0).data ().toString ();
271
272 auto tags = chosen;
273 for (const auto& tag : GetTags ())
274 if (!known.contains (tag) && !tags.contains (tag))
275 tags << tag;
276
277 Edit_.setText (tags.join (Separator_));
278 emit tagPicked ();
279 }
280
281 bool TagsEditorAddon::IsEdited () const
282 {
283 return !TagsChange { .Before_ = ShownTags_, .After_ = GetTags () }.IsEmpty ();
284 }
285
286 void TagsEditorAddon::ReportEdit ()
287 {
288 const TagsChange change { .Before_ = ShownTags_, .After_ = GetTags () };
289 if (change.IsEmpty ())
290 return;
291
292 ShownTags_ = change.After_;
293 emit tagsCommitted (change);
294 }
295}
Tags manager's interface.
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.
Manages additional overlay buttons in a QLineEdit.
void Add(QToolButton *button)
Adds a button to the line edit.
QStringList GetTags() const
Returns the tags in the text, split at the separator and trimmed (see SplitTags()).
void tagsCommitted(const TagsChange &change)
Emitted when the user has changed the tags, by a pick or by editing the text and then pressing Enter ...
void tagPicked()
Emitted after the text has been updated with a pick made in the selector or the completer,...
TagsEditorAddon(QLineEdit &edit, QAbstractItemModel &model, Flags flags=Flag::None)
Attaches to edit with model as the tags model; model is not owned.
void AddSelectorButton(LineEditButtonManager *manager=nullptr)
Adds a button, to manager or to a new one, opening a popup CategorySelectorWindow bound as via BindSe...
void BindSelector(CategorySelector &selector)
Binds selector to the edit.
QString GetSeparator() const
Returns the separator the tags are joined by, GetDefaultTagsSeparator() by default.
void SetSeparator(const QString &)
Sets the separator. The text is not updated.
void SetTags(const QStringList &tags)
Shows tags: sets the text to them joined by the separator and syncs the bound selector....
bool eventFilter(QObject *, QEvent *) override
void UpdateTags(const QStringList &tags)
Shows tags as via SetTags() unless the text differs from the shown tags.
QStringList SplitTags(QStringView text, const QString &separator)
Splits a string with tags into the tags.
Definition util.cpp:25
QString GetDefaultTagsSeparator()
Definition util.cpp:14
QString GetTagsDelimiter(const QString &separator)
Returns the delimiter the tags are split by for the given separator they are joined with.
Definition util.cpp:19
A change of a tags list, described by its old and new versions.
Definition tagschange.h:22
bool IsEmpty() const
Returns whether this change neither adds nor removes tags.