1 | /* |
2 | This file is part of Akonadi |
3 | |
4 | Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com> |
5 | |
6 | This library is free software; you can redistribute it and/or modify it |
7 | under the terms of the GNU Library General Public License as published by |
8 | the Free Software Foundation; either version 2 of the License, or (at your |
9 | option) any later version. |
10 | |
11 | This library is distributed in the hope that it will be useful, but WITHOUT |
12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
13 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public |
14 | License for more details. |
15 | |
16 | You should have received a copy of the GNU Library General Public License |
17 | along with this library; see the file COPYING.LIB. If not, write to the |
18 | Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
19 | 02110-1301, USA. |
20 | */ |
21 | #include "tageditwidget_p.h" |
22 | |
23 | #include <kicon.h> |
24 | #include <klineedit.h> |
25 | #include <klocalizedstring.h> |
26 | #include <kmessagebox.h> |
27 | #include <kcheckableproxymodel.h> |
28 | |
29 | #include <akonadi/changerecorder.h> |
30 | #include <akonadi/tagcreatejob.h> |
31 | #include <akonadi/tagdeletejob.h> |
32 | #include <akonadi/tagfetchscope.h> |
33 | #include <akonadi/tagattribute.h> |
34 | #include "tagmodel.h" |
35 | |
36 | #include <QEvent> |
37 | #include <QHBoxLayout> |
38 | #include <QLabel> |
39 | #include <QListWidget> |
40 | #include <QPushButton> |
41 | #include <QTimer> |
42 | #include <QVBoxLayout> |
43 | #include <QWidget> |
44 | |
45 | using namespace Akonadi; |
46 | |
47 | class TagEditWidget::Private : public QObject |
48 | { |
49 | Q_OBJECT |
50 | public: |
51 | Private(Akonadi::TagModel *model, QWidget *parent); |
52 | |
53 | public Q_SLOTS: |
54 | void slotTextEdited(const QString &text); |
55 | void slotItemEntered(const QModelIndex &index); |
56 | void showDeleteButton(); |
57 | void deleteTag(); |
58 | void slotCreateTag(); |
59 | void slotCreateTagFinished(KJob *job); |
60 | void onRowsInserted(const QModelIndex &parent, int start, int end); |
61 | |
62 | public: |
63 | void select(const QModelIndex &parent, int start, int end, QItemSelectionModel::SelectionFlag selectionFlag); |
64 | enum ItemType { |
65 | UrlTag = Qt::UserRole + 1 |
66 | }; |
67 | |
68 | QWidget *d; |
69 | Akonadi::Tag::List m_tags; |
70 | Akonadi::TagModel *m_model; |
71 | QListView *m_tagsView; |
72 | KCheckableProxyModel *m_checkableProxy; |
73 | QModelIndex m_deleteCandidate; |
74 | QPushButton *m_newTagButton; |
75 | KLineEdit *m_newTagEdit; |
76 | |
77 | QPushButton *m_deleteButton; |
78 | QTimer *m_deleteButtonTimer; |
79 | }; |
80 | |
81 | TagEditWidget::Private::Private(Akonadi::TagModel *model, QWidget *parent) |
82 | : QObject() |
83 | , d(parent) |
84 | , m_model(model) |
85 | , m_tagsView(0) |
86 | , m_newTagButton(0) |
87 | , m_newTagEdit(0) |
88 | , m_deleteButton(0) |
89 | , m_deleteButtonTimer(0) |
90 | { |
91 | |
92 | } |
93 | |
94 | void TagEditWidget::Private::select(const QModelIndex &parent, int start, int end, QItemSelectionModel::SelectionFlag selectionFlag) |
95 | { |
96 | QItemSelection selection; |
97 | for (int i = start; i <= end; i++) { |
98 | const QModelIndex index = m_model->index(i, 0, parent); |
99 | const Akonadi::Tag insertedTag = index.data(Akonadi::TagModel::TagRole).value<Akonadi::Tag>(); |
100 | if (m_tags.contains(insertedTag)) { |
101 | selection.select(index, index); |
102 | } |
103 | } |
104 | m_checkableProxy->selectionModel()->select(selection, selectionFlag); |
105 | } |
106 | |
107 | void TagEditWidget::Private::onRowsInserted(const QModelIndex &parent, int start, int end) |
108 | { |
109 | select(parent, start, end, QItemSelectionModel::Select); |
110 | } |
111 | |
112 | void TagEditWidget::Private::slotCreateTag() |
113 | { |
114 | Akonadi::TagCreateJob *createJob = new Akonadi::TagCreateJob(Akonadi::Tag(m_newTagEdit->text()), this); |
115 | connect(createJob, SIGNAL(finished(KJob*)), |
116 | this, SLOT(slotCreateTagFinished(KJob*))); |
117 | |
118 | m_newTagEdit->clear(); |
119 | m_newTagEdit->setEnabled(false); |
120 | m_newTagButton->setEnabled(false); |
121 | } |
122 | |
123 | void TagEditWidget::Private::slotCreateTagFinished(KJob *job) |
124 | { |
125 | if (job->error()) { |
126 | KMessageBox::error(d, i18n("An error occurred while creating a new tag" ), |
127 | i18n("Failed to create a new tag" )); |
128 | } |
129 | |
130 | m_newTagEdit->setEnabled(true); |
131 | } |
132 | |
133 | void TagEditWidget::Private::slotTextEdited(const QString &text) |
134 | { |
135 | // Remove unnecessary spaces from a new tag is |
136 | // mandatory, as the user cannot see the difference |
137 | // between a tag "Test" and "Test ". |
138 | const QString tagText = text.simplified(); |
139 | if (tagText.isEmpty()) { |
140 | m_newTagButton->setEnabled(false); |
141 | return; |
142 | } |
143 | |
144 | // Check whether the new tag already exists |
145 | const int count = m_model->rowCount(); |
146 | bool exists = false; |
147 | for (int i = 0; i < count; ++i) { |
148 | const QModelIndex index = m_model->index(i, 0, QModelIndex()); |
149 | if (index.data(Qt::DisplayRole).toString() == tagText) { |
150 | exists = true; |
151 | break; |
152 | } |
153 | } |
154 | m_newTagButton->setEnabled(!exists); |
155 | } |
156 | |
157 | void TagEditWidget::Private::slotItemEntered(const QModelIndex &index) |
158 | { |
159 | // align the delete-button to stay on the right border |
160 | // of the item |
161 | const QRect rect = m_tagsView->visualRect(index); |
162 | const int size = rect.height(); |
163 | const int x = rect.right() - size; |
164 | const int y = rect.top(); |
165 | m_deleteButton->move(x, y); |
166 | m_deleteButton->resize(size, size); |
167 | |
168 | m_deleteCandidate = index; |
169 | m_deleteButtonTimer->start(); |
170 | } |
171 | |
172 | void TagEditWidget::Private::showDeleteButton() |
173 | { |
174 | m_deleteButton->show(); |
175 | } |
176 | |
177 | void TagEditWidget::Private::deleteTag() |
178 | { |
179 | Q_ASSERT(m_deleteCandidate.isValid()); |
180 | const Akonadi::Tag tag = m_deleteCandidate.data(Akonadi::TagModel::TagRole).value<Akonadi::Tag>(); |
181 | const QString text = i18nc("@info" , |
182 | "Do you really want to remove the tag <resource>%1</resource>?" , |
183 | tag.name()); |
184 | const QString caption = i18nc("@title" , "Delete tag" ); |
185 | const KGuiItem deleteItem(i18nc("@action:button" , "Delete" ), KIcon(QLatin1String("edit-delete" ))); |
186 | const KGuiItem cancelItem(i18nc("@action:button" , "Cancel" ), KIcon(QLatin1String("dialog-cancel" ))); |
187 | if (KMessageBox::warningYesNo(d, text, caption, deleteItem, cancelItem) == KMessageBox::Yes) { |
188 | new Akonadi::TagDeleteJob(tag, this); |
189 | } |
190 | } |
191 | |
192 | TagEditWidget::TagEditWidget(Akonadi::TagModel *model, QWidget *parent, bool enableSelection) |
193 | : QWidget(parent) |
194 | , d(new Private(model, this)) |
195 | { |
196 | QVBoxLayout *topLayout = new QVBoxLayout(this); |
197 | |
198 | QItemSelectionModel *selectionModel = new QItemSelectionModel(d->m_model, this); |
199 | d->m_checkableProxy = new KCheckableProxyModel(this); |
200 | d->m_checkableProxy->setSourceModel(d->m_model); |
201 | d->m_checkableProxy->setSelectionModel(selectionModel); |
202 | connect(d->m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), d.data(), SLOT(onRowsInserted(QModelIndex,int,int))); |
203 | |
204 | d->m_tagsView = new QListView(this); |
205 | d->m_tagsView->setMouseTracking(true); |
206 | d->m_tagsView->setSelectionMode(QAbstractItemView::NoSelection); |
207 | d->m_tagsView->installEventFilter(this); |
208 | if (enableSelection) { |
209 | d->m_tagsView->setModel(d->m_checkableProxy); |
210 | } else { |
211 | d->m_tagsView->setModel(d->m_model); |
212 | } |
213 | connect(d->m_tagsView, SIGNAL(entered(QModelIndex)), |
214 | d.data(), SLOT(slotItemEntered(QModelIndex))); |
215 | |
216 | d->m_newTagEdit = new KLineEdit(this); |
217 | d->m_newTagEdit->setClearButtonShown(true); |
218 | connect(d->m_newTagEdit, SIGNAL(textEdited(QString)), |
219 | d.data(), SLOT(slotTextEdited(QString))); |
220 | |
221 | d->m_newTagButton = new QPushButton(i18nc("@label" , "Create new tag" )); |
222 | d->m_newTagButton->setEnabled(false); |
223 | connect(d->m_newTagButton , SIGNAL(clicked(bool)), |
224 | d.data(), SLOT(slotCreateTag())); |
225 | |
226 | QHBoxLayout *newTagLayout = new QHBoxLayout(); |
227 | newTagLayout->addWidget(d->m_newTagEdit, 1); |
228 | newTagLayout->addWidget(d->m_newTagButton); |
229 | |
230 | if (enableSelection) { |
231 | QLabel *label = new QLabel(i18nc("@label:textbox" , |
232 | "Configure which tags should " |
233 | "be applied." ), this); |
234 | topLayout->addWidget(label); |
235 | } |
236 | topLayout->addWidget(d->m_tagsView); |
237 | topLayout->addLayout(newTagLayout); |
238 | |
239 | setLayout(topLayout); |
240 | |
241 | // create the delete button, which is shown when |
242 | // hovering the items |
243 | d->m_deleteButton = new QPushButton(d->m_tagsView->viewport()); |
244 | d->m_deleteButton->setIcon(KIcon(QLatin1String("edit-delete" ))); |
245 | d->m_deleteButton->setToolTip(i18nc("@info" , "Delete tag" )); |
246 | d->m_deleteButton->hide(); |
247 | connect(d->m_deleteButton, SIGNAL(clicked()), d.data(), SLOT(deleteTag())); |
248 | |
249 | d->m_deleteButtonTimer = new QTimer(this); |
250 | d->m_deleteButtonTimer->setSingleShot(true); |
251 | d->m_deleteButtonTimer->setInterval(500); |
252 | connect(d->m_deleteButtonTimer, SIGNAL(timeout()), d.data(), SLOT(showDeleteButton())); |
253 | } |
254 | |
255 | TagEditWidget::~TagEditWidget() |
256 | { |
257 | |
258 | } |
259 | |
260 | void TagEditWidget::setSelection(const Akonadi::Tag::List &tags) |
261 | { |
262 | d->m_tags = tags; |
263 | d->select(QModelIndex(), 0, d->m_model->rowCount() - 1, QItemSelectionModel::ClearAndSelect); |
264 | } |
265 | |
266 | Akonadi::Tag::List TagEditWidget::selection() |
267 | { |
268 | Akonadi::Tag::List list; |
269 | for (int i = 0; i < d->m_checkableProxy->rowCount(); ++i) { |
270 | if (d->m_checkableProxy->selectionModel()->isRowSelected(i, QModelIndex())) { |
271 | const QModelIndex index = d->m_checkableProxy->index(i, 0, QModelIndex()); |
272 | const Akonadi::Tag tag = index.data(Akonadi::TagModel::TagRole).value<Akonadi::Tag>(); |
273 | list << tag; |
274 | } |
275 | } |
276 | return list; |
277 | } |
278 | |
279 | bool TagEditWidget::eventFilter(QObject *watched, QEvent *event) |
280 | { |
281 | if ((watched == d->m_tagsView) && (event->type() == QEvent::Leave)) { |
282 | d->m_deleteButtonTimer->stop(); |
283 | d->m_deleteButton->hide(); |
284 | } |
285 | return QWidget::eventFilter(watched, event); |
286 | } |
287 | |
288 | #include "tageditwidget.moc" |
289 | |