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 ¤tUrl, 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