1 /*****************************************************************************
2 * Copyright (C) 2010 by Peter Penz <peter.penz@gmx.at> *
3 * *
4 * This library is free software; you can redistribute it and/or *
5 * modify it under the terms of the GNU Library General Public *
6 * License as published by the Free Software Foundation; either *
7 * version 2 of the License, or (at your option) any later version. *
8 * *
9 * This library is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
12 * Library General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU Library General Public License *
15 * along with this library; see the file COPYING.LIB. If not, write to *
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, *
17 * Boston, MA 02110-1301, USA. *
18 *****************************************************************************/
19
20 #include "kfilemetadataprovider_p.h"
21
22 #include <kfileitem.h>
23 #include <kfilemetadatareader_p.h>
24 #include "knfotranslator_p.h"
25
26 #if ! KIO_NO_NEPOMUK
27 #define DISABLE_NEPOMUK_LEGACY
28 #include "nepomukmassupdatejob.h"
29 #include "tagwidget.h"
30 #include "tag.h"
31 #include "kratingwidget.h"
32 #include "resource.h"
33 #include "resourcemanager.h"
34
35 #include "kcommentwidget_p.h"
36 #else
37 namespace Nepomuk
38 {
39 typedef int Tag;
40 }
41 #endif
42
43 #include <QEvent>
44 #include <QLabel>
45 #include <QLocale>
46 #include <QUrl>
47
48 // Required includes for subDirectoriesCount():
49 #ifdef Q_OS_WIN
50 #include <QDir>
51 #else
52 #include <dirent.h>
53 #include <QFile>
54 #endif
55
56 namespace
57 {
plainText(const QString & richText)58 static QString plainText(const QString &richText)
59 {
60 QString plainText;
61 plainText.reserve(richText.length());
62
63 bool skip = false;
64 for (int i = 0; i < richText.length(); ++i) {
65 const QChar c = richText.at(i);
66 if (c == QLatin1Char('<')) {
67 skip = true;
68 } else if (c == QLatin1Char('>')) {
69 skip = false;
70 } else if (!skip) {
71 plainText.append(c);
72 }
73 }
74
75 return plainText;
76 }
77 }
78
79 // The default size hint of QLabel tries to return a square size.
80 // This does not work well in combination with layouts that use
81 // heightForWidth(): In this case it is possible that the content
82 // of a label might get clipped. By specifying a size hint
83 // with a maximum width that is necessary to contain the whole text,
84 // using heightForWidth() assures having a non-clipped text.
85 class ValueWidget : public QLabel
86 {
87 public:
88 explicit ValueWidget(QWidget *parent = 0);
89 virtual QSize sizeHint() const;
90 };
91
ValueWidget(QWidget * parent)92 ValueWidget::ValueWidget(QWidget *parent) :
93 QLabel(parent)
94 {
95 }
96
sizeHint() const97 QSize ValueWidget::sizeHint() const
98 {
99 QFontMetrics metrics(font());
100 // TODO: QLabel internally provides already a method sizeForWidth(),
101 // that would be sufficient. However this method is not accessible, so
102 // as workaround the tags from a richtext are removed manually here to
103 // have a proper size hint.
104 return metrics.size(Qt::TextSingleLine, plainText(text()));
105 }
106
107 class KFileMetaDataProvider::Private
108 {
109
110 public:
111 Private(KFileMetaDataProvider *parent);
112 ~Private();
113
114 void slotLoadingFinished();
115
116 void slotRatingChanged(unsigned int rating);
117 void slotTagsChanged(const QList<Nepomuk::Tag> &tags);
118 void slotCommentChanged(const QString &comment);
119
120 void slotMetaDataUpdateDone();
121 void slotTagClicked(const Nepomuk::Tag &tag);
122 void slotLinkActivated(const QString &link);
123
124 /**
125 * Disables the metadata widget and starts the job that
126 * changes the meta data asynchronously. After the job
127 * has been finished, the metadata widget gets enabled again.
128 */
129 void startChangeDataJob(KJob *job);
130
131 #if ! KIO_NO_NEPOMUK
132 QList<Nepomuk::Resource> resourceList() const;
133 QWidget *createRatingWidget(int rating, QWidget *parent);
134 QWidget *createTagWidget(const QList<Nepomuk::Tag> &tags, QWidget *parent);
135 QWidget *createCommentWidget(const QString &comment, QWidget *parent);
136 #endif
137 QWidget *createValueWidget(const QString &value, QWidget *parent);
138
139 /*
140 * @return The number of subdirectories for the directory \a path.
141 */
142 static int subDirectoriesCount(const QString &path);
143
144 bool m_readOnly;
145 bool m_nepomukActivated;
146 QList<KFileItem> m_fileItems;
147
148 #if ! KIO_NO_NEPOMUK
149 QHash<QUrl, Nepomuk::Variant> m_data;
150
151 QList<KFileMetaDataReader *> m_metaDataReaders;
152 KFileMetaDataReader *m_latestMetaDataReader;
153
154 QPointer<KRatingWidget> m_ratingWidget;
155 QPointer<Nepomuk::TagWidget> m_tagWidget;
156 QPointer<KCommentWidget> m_commentWidget;
157 #endif
158
159 private:
160 KFileMetaDataProvider *const q;
161 };
162
Private(KFileMetaDataProvider * parent)163 KFileMetaDataProvider::Private::Private(KFileMetaDataProvider *parent) :
164 m_readOnly(false),
165 m_nepomukActivated(false),
166 m_fileItems(),
167 #if ! KIO_NO_NEPOMUK
168 m_data(),
169 m_metaDataReaders(),
170 m_latestMetaDataReader(0),
171 m_ratingWidget(),
172 m_tagWidget(),
173 m_commentWidget(),
174 #endif
175 q(parent)
176 {
177 #if ! KIO_NO_NEPOMUK
178 m_nepomukActivated = Nepomuk::ResourceManager::instance()->initialized();
179 #endif
180 }
181
~Private()182 KFileMetaDataProvider::Private::~Private()
183 {
184 #if ! KIO_NO_NEPOMUK
185 qDeleteAll(m_metaDataReaders);
186 #endif
187 }
188
_k_fancyFormatDateTime(const QDateTime & dateTime)189 QString _k_fancyFormatDateTime(const QDateTime &dateTime)
190 {
191 QString dateStr;
192
193 // Only do Fancy if less than an hour into the future or less than a week in the past
194 if ((daysTo == 0 && secsTo > 3600) || daysTo < 0 || daysTo > 6) {
195 dateStr = dateTime.date().toString(Qt::DefaultLocaleLongDate);
196 } else {
197 switch (daysTo) {
198 case 0:
199 dateStr = i18n("Today");
200 break;
201 case 1:
202 dateStr = i18n("Yesterday");
203 break;
204 default:
205 dateStr = QLocale().dayName(dateTime.date().dayOfWeek());
206 }
207 }
208
209 return i18nc("concatenation of dates and time", "%1 %2", dateStr,
210 dateTime.time().toString(Qt::DefaultLocaleShortDate));
211 }
212
slotLoadingFinished()213 void KFileMetaDataProvider::Private::slotLoadingFinished()
214 {
215 #if ! KIO_NO_NEPOMUK
216 KFileMetaDataReader *finishedMetaDataReader = qobject_cast<KFileMetaDataReader *>(q->sender());
217 // The process that has emitted the finished() signal
218 // will get deleted and removed from m_metaDataReaders.
219 for (int i = 0; i < m_metaDataReaders.count(); ++i) {
220 KFileMetaDataReader *metaDataReader = m_metaDataReaders[i];
221 if (metaDataReader == finishedMetaDataReader) {
222 m_metaDataReaders.removeAt(i);
223 if (metaDataReader != m_latestMetaDataReader) {
224 // Ignore data of older processs, as the data got
225 // obsolete by m_latestMetaDataReader.
226 metaDataReader->deleteLater();
227 return;
228 }
229 }
230 }
231
232 m_data = m_latestMetaDataReader->metaData();
233 m_latestMetaDataReader->deleteLater();
234
235 if (m_fileItems.count() == 1) {
236 // TODO: Handle case if remote URLs are used properly. isDir() does
237 // not work, the modification date needs also to be adjusted...
238 const KFileItem &item = m_fileItems.first();
239
240 if (item.isDir()) {
241 const int count = subDirectoriesCount(item.url().pathOrUrl());
242 if (count == -1) {
243 m_data.insert(QUrl("kfileitem#size"), QString("Unknown"));
244 } else {
245 const QString itemCountString = i18ncp("@item:intable", "%1 item", "%1 items", count);
246 m_data.insert(QUrl("kfileitem#size"), itemCountString);
247 }
248 } else {
249 m_data.insert(QUrl("kfileitem#size"), KIO::convertSize(item.size()));
250 }
251 m_data.insert(QUrl("kfileitem#type"), item.mimeComment());
252 m_data.insert(QUrl("kfileitem#modified"), _k_fancyFormatDateTime(item.time(KFileItem::ModificationTime)));
253 m_data.insert(QUrl("kfileitem#owner"), item.user());
254 m_data.insert(QUrl("kfileitem#permissions"), item.permissionsString());
255 } else if (m_fileItems.count() > 1) {
256 // Calculate the size of all items
257 quint64 totalSize = 0;
258 foreach (const KFileItem &item, m_fileItems) {
259 if (!item.isDir() && !item.isLink()) {
260 totalSize += item.size();
261 }
262 }
263 m_data.insert(QUrl("kfileitem#totalSize"), KIO::convertSize(totalSize));
264 }
265 #endif
266
267 emit q->loadingFinished();
268 }
269
slotRatingChanged(unsigned int rating)270 void KFileMetaDataProvider::Private::slotRatingChanged(unsigned int rating)
271 {
272 #if ! KIO_NO_NEPOMUK
273 Nepomuk::MassUpdateJob *job = Nepomuk::MassUpdateJob::rateResources(resourceList(), rating);
274 startChangeDataJob(job);
275 #else
276 Q_UNUSED(rating);
277 #endif
278 }
279
slotTagsChanged(const QList<Nepomuk::Tag> & tags)280 void KFileMetaDataProvider::Private::slotTagsChanged(const QList<Nepomuk::Tag> &tags)
281 {
282 #if ! KIO_NO_NEPOMUK
283 if (!m_tagWidget.isNull()) {
284 m_tagWidget.data()->setSelectedTags(tags);
285
286 Nepomuk::MassUpdateJob *job = Nepomuk::MassUpdateJob::tagResources(resourceList(), tags);
287 startChangeDataJob(job);
288 }
289 #else
290 Q_UNUSED(tags);
291 #endif
292 }
293
slotCommentChanged(const QString & comment)294 void KFileMetaDataProvider::Private::slotCommentChanged(const QString &comment)
295 {
296 #if ! KIO_NO_NEPOMUK
297 Nepomuk::MassUpdateJob *job = Nepomuk::MassUpdateJob::commentResources(resourceList(), comment);
298 startChangeDataJob(job);
299 #else
300 Q_UNUSED(comment);
301 #endif
302 }
303
slotTagClicked(const Nepomuk::Tag & tag)304 void KFileMetaDataProvider::Private::slotTagClicked(const Nepomuk::Tag &tag)
305 {
306 #if ! KIO_NO_NEPOMUK
307 emit q->urlActivated(tag.resourceUri());
308 #else
309 Q_UNUSED(tag);
310 #endif
311 }
312
slotLinkActivated(const QString & link)313 void KFileMetaDataProvider::Private::slotLinkActivated(const QString &link)
314 {
315 emit q->urlActivated(QUrl(link));
316 }
317
startChangeDataJob(KJob * job)318 void KFileMetaDataProvider::Private::startChangeDataJob(KJob *job)
319 {
320 connect(job, SIGNAL(result(KJob*)),
321 q, SIGNAL(dataChangeFinished()));
322 emit q->dataChangeStarted();
323 job->start();
324 }
325
326 #if ! KIO_NO_NEPOMUK
resourceList() const327 QList<Nepomuk::Resource> KFileMetaDataProvider::Private::resourceList() const
328 {
329 QList<Nepomuk::Resource> list;
330 foreach (const KFileItem &item, m_fileItems) {
331 const QUrl url = item.nepomukUri();
332 if (url.isValid()) {
333 list.append(Nepomuk::Resource(url));
334 }
335 }
336 return list;
337 }
338
createRatingWidget(int rating,QWidget * parent)339 QWidget *KFileMetaDataProvider::Private::createRatingWidget(int rating, QWidget *parent)
340 {
341 KRatingWidget *ratingWidget = new KRatingWidget(parent);
342 const Qt::Alignment align = (ratingWidget->layoutDirection() == Qt::LeftToRight) ?
343 Qt::AlignLeft : Qt::AlignRight;
344 ratingWidget->setAlignment(align);
345 ratingWidget->setRating(rating);
346 const QFontMetrics metrics(parent->font());
347 ratingWidget->setPixmapSize(metrics.height());
348
349 connect(ratingWidget, SIGNAL(ratingChanged(uint)),
350 q, SLOT(slotRatingChanged(uint)));
351
352 m_ratingWidget = ratingWidget;
353
354 return ratingWidget;
355 }
356
createTagWidget(const QList<Nepomuk::Tag> & tags,QWidget * parent)357 QWidget *KFileMetaDataProvider::Private::createTagWidget(const QList<Nepomuk::Tag> &tags, QWidget *parent)
358 {
359 Nepomuk::TagWidget *tagWidget = new Nepomuk::TagWidget(parent);
360 tagWidget->setModeFlags(m_readOnly
361 ? Nepomuk::TagWidget::MiniMode | Nepomuk::TagWidget::ReadOnly
362 : Nepomuk::TagWidget::MiniMode);
363 tagWidget->setSelectedTags(tags);
364
365 connect(tagWidget, SIGNAL(selectionChanged(QList<Nepomuk::Tag>)),
366 q, SLOT(slotTagsChanged(QList<Nepomuk::Tag>)));
367 connect(tagWidget, SIGNAL(tagClicked(Nepomuk::Tag)),
368 q, SLOT(slotTagClicked(Nepomuk::Tag)));
369
370 m_tagWidget = tagWidget;
371
372 return tagWidget;
373 }
374
createCommentWidget(const QString & comment,QWidget * parent)375 QWidget *KFileMetaDataProvider::Private::createCommentWidget(const QString &comment, QWidget *parent)
376 {
377 KCommentWidget *commentWidget = new KCommentWidget(parent);
378 commentWidget->setText(comment);
379 commentWidget->setReadOnly(m_readOnly);
380
381 connect(commentWidget, SIGNAL(commentChanged(QString)),
382 q, SLOT(slotCommentChanged(QString)));
383
384 m_commentWidget = commentWidget;
385
386 return commentWidget;
387 }
388 #endif
389
createValueWidget(const QString & value,QWidget * parent)390 QWidget *KFileMetaDataProvider::Private::createValueWidget(const QString &value, QWidget *parent)
391 {
392 ValueWidget *valueWidget = new ValueWidget(parent);
393 valueWidget->setWordWrap(true);
394 valueWidget->setAlignment(Qt::AlignTop | Qt::AlignLeft);
395 valueWidget->setText(m_readOnly ? plainText(value) : value);
396 connect(valueWidget, SIGNAL(linkActivated(QString)), q, SLOT(slotLinkActivated(QString)));
397 return valueWidget;
398 }
399
KFileMetaDataProvider(QObject * parent)400 KFileMetaDataProvider::KFileMetaDataProvider(QObject *parent) :
401 QObject(parent),
402 d(new Private(this))
403 {
404 }
405
~KFileMetaDataProvider()406 KFileMetaDataProvider::~KFileMetaDataProvider()
407 {
408 delete d;
409 }
410
setItems(const KFileItemList & items)411 void KFileMetaDataProvider::setItems(const KFileItemList &items)
412 {
413 d->m_fileItems = items;
414
415 #if ! KIO_NO_NEPOMUK
416 if (items.isEmpty()) {
417 return;
418 }
419 Q_PRIVATE_SLOT(d,void slotDataChangeStarted())
420 Q_PRIVATE_SLOT(d,void slotDataChangeFinished())
421 QList<QUrl> urls;
422 foreach (const KFileItem &item, items) {
423 const QUrl url = item.nepomukUri();
424 if (url.isValid()) {
425 urls.append(url);
426 }
427 }
428
429 d->m_latestMetaDataReader = new KFileMetaDataReader(urls);
430 d->m_latestMetaDataReader->setReadContextData(d->m_nepomukActivated);
431 connect(d->m_latestMetaDataReader, SIGNAL(finished()), this, SLOT(slotLoadingFinished()));
432 d->m_metaDataReaders.append(d->m_latestMetaDataReader);
433 d->m_latestMetaDataReader->start();
434 #endif
435 }
436
label(const QUrl & metaDataUri) const437 QString KFileMetaDataProvider::label(const QUrl &metaDataUri) const
438 {
439 struct TranslationItem {
440 const char *const key;
441 const char *const context;
442 const char *const value;
443 };
444
445 static const TranslationItem translations[] = {
446 { "kfileitem#comment", I18N_NOOP2_NOSTRIP("@label", "Comment") },
447 { "kfileitem#modified", I18N_NOOP2_NOSTRIP("@label", "Modified") },
448 { "kfileitem#owner", I18N_NOOP2_NOSTRIP("@label", "Owner") },
449 { "kfileitem#permissions", I18N_NOOP2_NOSTRIP("@label", "Permissions") },
450 { "kfileitem#rating", I18N_NOOP2_NOSTRIP("@label", "Rating") },
451 { "kfileitem#size", I18N_NOOP2_NOSTRIP("@label", "Size") },
452 { "kfileitem#tags", I18N_NOOP2_NOSTRIP("@label", "Tags") },
453 { "kfileitem#totalSize", I18N_NOOP2_NOSTRIP("@label", "Total Size") },
454 { "kfileitem#type", I18N_NOOP2_NOSTRIP("@label", "Type") },
455 { 0, 0, 0} // Mandatory last entry
456 };
457
458 static QHash<QString, QString> hash;
459 if (hash.isEmpty()) {
460 const TranslationItem *item = &translations[0];
461 while (item->key != 0) {
462 hash.insert(item->key, i18nc(item->context, item->value));
463 ++item;
464 }
465 }
466
467 QString value = hash.value(metaDataUri.url());
468 if (value.isEmpty()) {
469 value = KNfoTranslator::instance().translation(metaDataUri);
470 }
471
472 return value;
473 }
474
group(const QUrl & metaDataUri) const475 QString KFileMetaDataProvider::group(const QUrl &metaDataUri) const
476 {
477 QString group; // return value
478
479 const QString uri = metaDataUri.url();
480 if (uri == QLatin1String("kfileitem#type")) {
481 group = QLatin1String("0FileItemA");
482 } else if (uri == QLatin1String("kfileitem#size")) {
483 group = QLatin1String("0FileItemB");
484 } else if (uri == QLatin1String("http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#width")) {
485 group = QLatin1String("0SizeA");
486 } else if (uri == QLatin1String("http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#height")) {
487 group = QLatin1String("0SizeB");
488 }
489
490 return group;
491 }
492
items() const493 KFileItemList KFileMetaDataProvider::items() const
494 {
495 return d->m_fileItems;
496 }
497
setReadOnly(bool readOnly)498 void KFileMetaDataProvider::setReadOnly(bool readOnly)
499 {
500 d->m_readOnly = readOnly;
501 }
502
isReadOnly() const503 bool KFileMetaDataProvider::isReadOnly() const
504 {
505 return d->m_readOnly;
506 }
507
508 #if ! KIO_NO_NEPOMUK
data() const509 QHash<QUrl, Nepomuk::Variant> KFileMetaDataProvider::data() const
510 {
511 return d->m_data;
512 }
513
createValueWidget(const QUrl & metaDataUri,const Nepomuk::Variant & value,QWidget * parent) const514 QWidget *KFileMetaDataProvider::createValueWidget(const QUrl &metaDataUri,
515 const Nepomuk::Variant &value,
516 QWidget *parent) const
517 {
518 Q_ASSERT(parent != 0);
519 QWidget *widget = 0;
520
521 if (d->m_nepomukActivated) {
522 const QString uri = metaDataUri.url();
523 if (uri == QLatin1String("kfileitem#rating")) {
524 widget = d->createRatingWidget(value.toInt(), parent);
525 } else if (uri == QLatin1String("kfileitem#tags")) {
526 const QStringList tagNames = value.toStringList();
527 QList<Nepomuk::Tag> tags;
528 foreach (const QString &tagName, tagNames) {
529 tags.append(Nepomuk::Tag(tagName));
530 }
531
532 widget = d->createTagWidget(tags, parent);
533 } else if (uri == QLatin1String("kfileitem#comment")) {
534 widget = d->createCommentWidget(value.toString(), parent);
535 }
536 }
537
538 if (widget == 0) {
539 widget = d->createValueWidget(value.toString(), parent);
540 }
541
542 widget->setForegroundRole(parent->foregroundRole());
543 widget->setFont(parent->font());
544
545 return widget;
546 }
547 #endif
548
subDirectoriesCount(const QString & path)549 int KFileMetaDataProvider::Private::subDirectoriesCount(const QString &path)
550 {
551 #ifdef Q_OS_WIN
552 QDir dir(path);
553 return dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System).count();
554 #else
555 // Taken from kdelibs/kio/kio/kdirmodel.cpp
556 // Copyright (C) 2006 David Faure <faure@kde.org>
557
558 int count = -1;
559 DIR *dir = ::opendir(QFile::encodeName(path));
560 if (dir) {
561 count = 0;
562 struct dirent *dirEntry = 0;
563 while ((dirEntry = ::readdir(dir))) { // krazy:exclude=syscalls
564 if (dirEntry->d_name[0] == '.') {
565 if (dirEntry->d_name[1] == '\0') {
566 // Skip "."
567 continue;
568 }
569 if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') {
570 // Skip ".."
571 continue;
572 }
573 }
574 ++count;
575 }
576 ::closedir(dir);
577 }
578 return count;
579 #endif
580 }
581
582 #include "moc_kfilemetadataprovider_p.cpp"
583