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