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
45using namespace Akonadi;
46
47class TagEditWidget::Private : public QObject
48{
49 Q_OBJECT
50public:
51 Private(Akonadi::TagModel *model, QWidget *parent);
52
53public 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
62public:
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
81TagEditWidget::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
94void 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
107void TagEditWidget::Private::onRowsInserted(const QModelIndex &parent, int start, int end)
108{
109 select(parent, start, end, QItemSelectionModel::Select);
110}
111
112void 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
123void 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
133void 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
157void 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
172void TagEditWidget::Private::showDeleteButton()
173{
174 m_deleteButton->show();
175}
176
177void 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
192TagEditWidget::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
255TagEditWidget::~TagEditWidget()
256{
257
258}
259
260void 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
266Akonadi::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
279bool 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