1 // vim: set tabstop=4 shiftwidth=4 expandtab:
2 /*
3 Gwenview: an image viewer
4 Copyright 2000-2008 Aurélien Gâteau <agateau@kde.org>
5 Copyright 2008      Angelo Naselli  <anaselli@linux.it>
6 
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; either version 2
10 of the License, or (at your option) any later version.
11 
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 GNU General Public License for more details.
16 
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
20 
21 */
22 #include "kipiinterface.h"
23 
24 // Qt
25 #include <QAction>
26 #include <QDesktopServices>
27 #include <QFileSystemWatcher>
28 #include <QList>
29 #include <QMenu>
30 #include <QRegExp>
31 #include <QTimer>
32 #include <QUrl>
33 
34 // KF
35 #include <KActionCollection>
36 #include <KDirLister>
37 #include <KIO/DesktopExecParser>
38 #include <KLocalizedString>
39 #include <KXMLGUIFactory>
40 
41 // KIPI
42 #include <kipi/imageinfo.h>
43 #include <kipi/imageinfoshared.h>
44 #include <kipi/pluginloader.h>
45 
46 // local
47 #include "gwenview_app_debug.h"
48 #include "kipiimagecollectionselector.h"
49 #include "kipiuploadwidget.h"
50 #include "mainwindow.h"
51 #include <lib/contextmanager.h>
52 #include <lib/jpegcontent.h>
53 #include <lib/mimetypeutils.h>
54 #include <lib/semanticinfo/sorteddirmodel.h>
55 #include <lib/timeutils.h>
56 
57 #define KIPI_PLUGINS_URL QStringLiteral("appstream://org.kde.kipi_plugins")
58 
59 namespace Gwenview
60 {
61 #undef ENABLE_LOG
62 #undef LOG
63 
64 //#define ENABLE_LOG
65 #ifdef ENABLE_LOG
66 #define LOG(x) qCDebug(GWENVIEW_APP_LOG) << x
67 #else
68 #define LOG(x) ;
69 #endif
70 
71 class KIPIImageInfo : public KIPI::ImageInfoShared
72 {
73     static const QRegExp sExtensionRE;
74 
75 public:
KIPIImageInfo(KIPI::Interface * interface,const QUrl & url)76     KIPIImageInfo(KIPI::Interface *interface, const QUrl &url)
77         : KIPI::ImageInfoShared(interface, url)
78     {
79         KFileItem item(url);
80 
81         mAttributes.insert(QStringLiteral("name"), url.fileName());
82         mAttributes.insert(QStringLiteral("comment"), comment());
83         mAttributes.insert(QStringLiteral("date"), TimeUtils::dateTimeForFileItem(item));
84         mAttributes.insert(QStringLiteral("orientation"), orientation());
85         mAttributes.insert(QStringLiteral("title"), prettyFileName());
86         int size = item.size();
87         if (size > 0) {
88             mAttributes.insert(QStringLiteral("filesize"), size);
89         }
90     }
91 
attributes()92     QMap<QString, QVariant> attributes() override
93     {
94         return mAttributes;
95     }
96 
delAttributes(const QStringList & attributeNames)97     void delAttributes(const QStringList &attributeNames) override
98     {
99         for (const QString &name : attributeNames) {
100             mAttributes.remove(name);
101         }
102     }
103 
clearAttributes()104     void clearAttributes() override
105     {
106         mAttributes.clear();
107     }
108 
addAttributes(const QVariantMap & attributes)109     void addAttributes(const QVariantMap &attributes) override
110     {
111         QVariantMap::ConstIterator it = attributes.constBegin(), end = attributes.constEnd();
112         for (; it != end; ++it) {
113             mAttributes.insert(it.key(), it.value());
114         }
115     }
116 
117 private:
prettyFileName() const118     QString prettyFileName() const
119     {
120         QString txt = _url.fileName();
121         txt.replace('_', ' ');
122         txt.remove(sExtensionRE);
123         return txt;
124     }
125 
comment() const126     QString comment() const
127     {
128         if (!_url.isLocalFile())
129             return QString();
130 
131         JpegContent content;
132         bool ok = content.load(_url.toLocalFile());
133         if (!ok)
134             return QString();
135 
136         return content.comment();
137     }
138 
orientation() const139     int orientation() const
140     {
141 #if 0 // PORT QT5
142         KFileMetaInfo metaInfo(_url);
143 
144         if (!metaInfo.isValid()) {
145             return 0;
146         }
147 
148         const KFileMetaInfoItem& mii = metaInfo.item("http://freedesktop.org/standards/xesam/1.0/core#orientation");
149         bool ok = false;
150         const Orientation orientation = (Orientation)mii.value().toInt(&ok);
151         if (!ok) {
152             return 0;
153         }
154 
155         switch (orientation) {
156         case NOT_AVAILABLE:
157         case NORMAL:
158             return 0;
159 
160         case ROT_90:
161             return 90;
162 
163         case ROT_180:
164             return 180;
165 
166         case ROT_270:
167             return 270;
168 
169         case HFLIP:
170         case VFLIP:
171         case TRANSPOSE:
172         case TRANSVERSE:
173             qCWarning(GWENVIEW_APP_LOG) << "Can't represent an orientation value of" << orientation << "as an angle (" << _url << ')';
174             return 0;
175         }
176 #endif
177         // qCWarning(GWENVIEW_APP_LOG) << "Don't know how to handle an orientation value of" << orientation << '(' << _url << ')';
178         return 0;
179     }
180 
181     QVariantMap mAttributes;
182 };
183 
184 const QRegExp KIPIImageInfo::sExtensionRE("\\.[a-z0-9]+$", Qt::CaseInsensitive);
185 
186 struct MenuInfo {
187     QString mName;
188     QList<QAction *> mActions;
189     QString mIconName;
190 
MenuInfoGwenview::MenuInfo191     MenuInfo()
192     {
193     }
194 
MenuInfoGwenview::MenuInfo195     MenuInfo(const QString &name, const QString &iconName)
196         : mName(name)
197         , mIconName(iconName)
198     {
199     }
200 };
201 using MenuInfoMap = QMap<KIPI::Category, MenuInfo>;
202 
203 struct KIPIInterfacePrivate {
204     KIPIInterface *q;
205     MainWindow *mMainWindow;
206     QMenu *mPluginMenu;
207     KIPI::PluginLoader *mPluginLoader;
208     KIPI::PluginLoader::PluginList mPluginQueue;
209     MenuInfoMap mMenuInfoMap;
210     QAction *mLoadingAction;
211     QAction *mNoPluginAction;
212     QAction *mInstallPluginAction;
213     QFileSystemWatcher mPluginWatcher;
214     QTimer mPluginLoadTimer;
215 
setupPluginsMenuGwenview::KIPIInterfacePrivate216     void setupPluginsMenu()
217     {
218         mPluginMenu = static_cast<QMenu *>(mMainWindow->factory()->container("plugins", mMainWindow));
219         QObject::connect(mPluginMenu, &QMenu::aboutToShow, q, &KIPIInterface::loadPlugins);
220     }
221 
createDummyPluginActionGwenview::KIPIInterfacePrivate222     QAction *createDummyPluginAction(const QString &text)
223     {
224         auto *action = new QAction(q);
225         action->setText(text);
226         // PORT QT5 action->setShortcutConfigurable(false);
227         action->setEnabled(false);
228         return action;
229     }
230 };
231 
KIPIInterface(MainWindow * mainWindow)232 KIPIInterface::KIPIInterface(MainWindow *mainWindow)
233     : KIPI::Interface(mainWindow)
234     , d(new KIPIInterfacePrivate)
235 {
236     d->q = this;
237     d->mMainWindow = mainWindow;
238     d->mPluginLoader = nullptr;
239     d->mLoadingAction = d->createDummyPluginAction(i18n("Loading..."));
240     d->mNoPluginAction = d->createDummyPluginAction(i18n("No Plugin Found"));
241     d->mInstallPluginAction = d->createDummyPluginAction(i18nc("@item:inmenu", "Install Plugins"));
242     connect(&d->mPluginLoadTimer, &QTimer::timeout, this, &KIPIInterface::loadPlugins);
243 
244     d->setupPluginsMenu();
245     QObject::connect(d->mMainWindow->contextManager(), &ContextManager::selectionChanged, this, &KIPIInterface::slotSelectionChanged);
246     QObject::connect(d->mMainWindow->contextManager(), &ContextManager::currentDirUrlChanged, this, &KIPIInterface::slotDirectoryChanged);
247 #if 0
248 //TODO instead of delaying can we load them all at start-up to use actions somewhere else?
249 // delay a bit, so that it's called after loadPlugins()
250     QTimer::singleShot(0, this, SLOT(init()));
251 #endif
252 }
253 
~KIPIInterface()254 KIPIInterface::~KIPIInterface()
255 {
256     delete d;
257 }
258 
actionLessThan(QAction * a1,QAction * a2)259 static bool actionLessThan(QAction *a1, QAction *a2)
260 {
261     QString a1Text = a1->text().remove('&');
262     QString a2Text = a2->text().remove('&');
263     return QString::compare(a1Text, a2Text, Qt::CaseInsensitive) < 0;
264 }
265 
loadPlugins()266 void KIPIInterface::loadPlugins()
267 {
268     // Already done
269     if (d->mPluginLoader) {
270         return;
271     }
272 
273     d->mMenuInfoMap[KIPI::ImagesPlugin] = MenuInfo(i18nc("@title:menu", "Images"), QStringLiteral("viewimage"));
274     d->mMenuInfoMap[KIPI::ToolsPlugin] = MenuInfo(i18nc("@title:menu", "Tools"), QStringLiteral("tools"));
275     d->mMenuInfoMap[KIPI::ImportPlugin] = MenuInfo(i18nc("@title:menu", "Import"), QStringLiteral("document-import"));
276     d->mMenuInfoMap[KIPI::ExportPlugin] = MenuInfo(i18nc("@title:menu", "Export"), QStringLiteral("document-export"));
277     d->mMenuInfoMap[KIPI::BatchPlugin] = MenuInfo(i18nc("@title:menu", "Batch Processing"), QStringLiteral("editimage"));
278     d->mMenuInfoMap[KIPI::CollectionsPlugin] = MenuInfo(i18nc("@title:menu", "Collections"), QStringLiteral("view-list-symbolic"));
279 
280     d->mPluginLoader = new KIPI::PluginLoader();
281     d->mPluginLoader->setInterface(this);
282     d->mPluginLoader->init();
283     d->mPluginQueue = d->mPluginLoader->pluginList();
284     d->mPluginMenu->addAction(d->mLoadingAction);
285     loadOnePlugin();
286 }
287 
loadOnePlugin()288 void KIPIInterface::loadOnePlugin()
289 {
290     while (!d->mPluginQueue.isEmpty()) {
291         KIPI::PluginLoader::Info *pluginInfo = d->mPluginQueue.takeFirst();
292         if (!pluginInfo->shouldLoad()) {
293             continue;
294         }
295 
296         KIPI::Plugin *plugin = pluginInfo->plugin();
297         if (!plugin) {
298             qCWarning(GWENVIEW_APP_LOG) << "Plugin from library" << pluginInfo->library() << "failed to load";
299             continue;
300         }
301 
302         plugin->setup(d->mMainWindow);
303         const QList<QAction *> actions = plugin->actions();
304         for (QAction *action : actions) {
305             KIPI::Category category = plugin->category(action);
306 
307             if (!d->mMenuInfoMap.contains(category)) {
308                 qCWarning(GWENVIEW_APP_LOG) << "Unknown category '" << category;
309                 continue;
310             }
311 
312             d->mMenuInfoMap[category].mActions << action;
313         }
314         // FIXME: Port
315         // plugin->actionCollection()->readShortcutSettings();
316 
317         // If we reach this point, we just loaded one plugin. Go back to the
318         // event loop. We will come back to load the remaining plugins or create
319         // the menu later
320         QMetaObject::invokeMethod(this, &KIPIInterface::loadOnePlugin, Qt::QueuedConnection);
321         return;
322     }
323 
324     // If we reach this point, all plugins have been loaded. We can fill the
325     // menu
326     MenuInfoMap::Iterator it = d->mMenuInfoMap.begin(), end = d->mMenuInfoMap.end();
327     for (; it != end; ++it) {
328         MenuInfo &info = it.value();
329         if (!info.mActions.isEmpty()) {
330             QMenu *menu = d->mPluginMenu->addMenu(info.mName);
331             menu->setIcon(QIcon::fromTheme(info.mIconName));
332             std::sort(info.mActions.begin(), info.mActions.end(), actionLessThan);
333             for (QAction *action : qAsConst(info.mActions)) {
334                 menu->addAction(action);
335             }
336         }
337     }
338 
339     d->mPluginMenu->removeAction(d->mLoadingAction);
340     if (d->mPluginMenu->isEmpty()) {
341         if (KIO::DesktopExecParser::hasSchemeHandler(QUrl(KIPI_PLUGINS_URL))) {
342             d->mPluginMenu->addAction(d->mInstallPluginAction);
343             d->mInstallPluginAction->setEnabled(true);
344             QObject::connect(d->mInstallPluginAction, &QAction::triggered, this, [&]() {
345                 QDesktopServices::openUrl(QUrl(KIPI_PLUGINS_URL));
346                 d->mPluginWatcher.addPaths(QCoreApplication::libraryPaths());
347                 connect(&d->mPluginWatcher, &QFileSystemWatcher::directoryChanged, this, &KIPIInterface::packageFinished);
348             });
349         } else {
350             d->mPluginMenu->addAction(d->mNoPluginAction);
351         }
352     }
353 
354     loadingFinished();
355 }
356 
packageFinished()357 void KIPIInterface::packageFinished()
358 {
359     if (d->mPluginLoader) {
360         delete d->mPluginLoader;
361         d->mPluginLoader = nullptr;
362     }
363     d->mPluginMenu->removeAction(d->mInstallPluginAction);
364     d->mPluginMenu->removeAction(d->mNoPluginAction);
365     d->mPluginLoadTimer.start(1000);
366 }
367 
pluginActions(KIPI::Category category) const368 QList<QAction *> KIPIInterface::pluginActions(KIPI::Category category) const
369 {
370     const_cast<KIPIInterface *>(this)->loadPlugins();
371 
372     if (isLoadingFinished()) {
373         QList<QAction *> list = d->mMenuInfoMap.value(category).mActions;
374         if (list.isEmpty()) {
375             if (KIO::DesktopExecParser::hasSchemeHandler(QUrl(KIPI_PLUGINS_URL))) {
376                 list << d->mInstallPluginAction;
377             } else {
378                 list << d->mNoPluginAction;
379             }
380         }
381         return list;
382     } else {
383         return QList<QAction *>() << d->mLoadingAction;
384     }
385 }
386 
isLoadingFinished() const387 bool KIPIInterface::isLoadingFinished() const
388 {
389     if (!d->mPluginLoader) {
390         // Not even started
391         return false;
392     }
393     return d->mPluginQueue.isEmpty();
394 }
395 
init()396 void KIPIInterface::init()
397 {
398     slotDirectoryChanged();
399     slotSelectionChanged();
400 }
401 
currentAlbum()402 KIPI::ImageCollection KIPIInterface::currentAlbum()
403 {
404     LOG("");
405     const ContextManager *contextManager = d->mMainWindow->contextManager();
406     const QUrl url = contextManager->currentDirUrl();
407     const SortedDirModel *model = contextManager->dirModel();
408 
409     QList<QUrl> list;
410     const int count = model->rowCount();
411     for (int row = 0; row < count; ++row) {
412         const QModelIndex &index = model->index(row, 0);
413         const KFileItem item = model->itemForIndex(index);
414         if (MimeTypeUtils::fileItemKind(item) == MimeTypeUtils::KIND_RASTER_IMAGE) {
415             list << item.targetUrl();
416         }
417     }
418 
419     return KIPI::ImageCollection(new ImageCollection(url, url.fileName(), list));
420 }
421 
currentSelection()422 KIPI::ImageCollection KIPIInterface::currentSelection()
423 {
424     LOG("");
425 
426     KFileItemList fileList = d->mMainWindow->contextManager()->selectedFileItemList();
427     QList<QUrl> list = fileList.urlList();
428     QUrl url = d->mMainWindow->contextManager()->currentUrl();
429 
430     return KIPI::ImageCollection(new ImageCollection(url, url.fileName(), list));
431 }
432 
allAlbums()433 QList<KIPI::ImageCollection> KIPIInterface::allAlbums()
434 {
435     LOG("");
436     QList<KIPI::ImageCollection> list;
437     list << currentAlbum() << currentSelection();
438     return list;
439 }
440 
info(const QUrl & url)441 KIPI::ImageInfo KIPIInterface::info(const QUrl &url)
442 {
443     LOG("");
444     return KIPI::ImageInfo(new KIPIImageInfo(this, url));
445 }
446 
features() const447 int KIPIInterface::features() const
448 {
449     return KIPI::HostAcceptNewImages;
450 }
451 
452 /**
453  * KDirLister will pick up the image if necessary, so no updating is needed
454  * here, it is however necessary to discard caches if the plugin preserves timestamp
455  */
addImage(const QUrl &,QString &)456 bool KIPIInterface::addImage(const QUrl &, QString &)
457 {
458     // TODO  setContext(const QUrl &currentUrl, const KFileItemList& selection)?
459     // Cache::instance()->invalidate( url );
460     return true;
461 }
462 
delImage(const QUrl &)463 void KIPIInterface::delImage(const QUrl &)
464 {
465     // TODO
466 }
467 
refreshImages(const QList<QUrl> &)468 void KIPIInterface::refreshImages(const QList<QUrl> &)
469 {
470     // TODO
471 }
472 
imageCollectionSelector(QWidget * parent)473 KIPI::ImageCollectionSelector *KIPIInterface::imageCollectionSelector(QWidget *parent)
474 {
475     return new KIPIImageCollectionSelector(this, parent);
476 }
477 
uploadWidget(QWidget * parent)478 KIPI::UploadWidget *KIPIInterface::uploadWidget(QWidget *parent)
479 {
480     return (new KIPIUploadWidget(this, parent));
481 }
482 
slotSelectionChanged()483 void KIPIInterface::slotSelectionChanged()
484 {
485     Q_EMIT selectionChanged(!d->mMainWindow->contextManager()->selectedFileItemList().isEmpty());
486 }
487 
slotDirectoryChanged()488 void KIPIInterface::slotDirectoryChanged()
489 {
490     Q_EMIT currentAlbumChanged(true);
491 }
492 
493 #ifdef GWENVIEW_KIPI_WITH_CREATE_METHODS
createReadWriteLock(const QUrl & url) const494 KIPI::FileReadWriteLock *KIPIInterface::createReadWriteLock(const QUrl &url) const
495 {
496     Q_UNUSED(url);
497     return nullptr;
498 }
499 
createMetadataProcessor() const500 KIPI::MetadataProcessor *KIPIInterface::createMetadataProcessor() const
501 {
502     return nullptr;
503 }
504 
505 #ifdef GWENVIEW_KIPI_WITH_CREATE_RAW_PROCESSOR
createRawProcessor() const506 KIPI::RawProcessor *KIPIInterface::createRawProcessor() const
507 {
508     return NULL;
509 }
510 #endif
511 #endif
512 
513 } // namespace
514