1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2011-01-24
7  * Description : Tags Action Manager
8  *
9  * Copyright (C) 2011-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10  *
11  * This program is free software; you can redistribute it
12  * and/or modify it under the terms of the GNU General
13  * Public License as published by the Free Software Foundation;
14  * either version 2, or (at your option)
15  * any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * ============================================================ */
23 
24 #include "tagsactionmngr.h"
25 
26 // Qt includes
27 
28 #include <QList>
29 #include <QShortcut>
30 #include <QIcon>
31 #include <QKeySequence>
32 #include <QApplication>
33 #include <QAction>
34 
35 // KDE includes
36 
37 #include <klocalizedstring.h>
38 #include <kactioncollection.h>
39 
40 // Local includes
41 
42 #include "digikam_debug.h"
43 #include "album.h"
44 #include "coredb.h"
45 #include "albummanager.h"
46 #include "coredbaccess.h"
47 #include "coredbconstants.h"
48 #include "coredbwatch.h"
49 #include "coredbinfocontainers.h"
50 #include "digikamapp.h"
51 #include "dxmlguiwindow.h"
52 #include "itemiconview.h"
53 #include "imagewindow.h"
54 #include "lighttablewindow.h"
55 #include "picklabelwidget.h"
56 #include "colorlabelwidget.h"
57 #include "tagscache.h"
58 #include "tagproperties.h"
59 #include "ratingwidget.h"
60 #include "syncjob.h"
61 
62 namespace Digikam
63 {
64 
65 TagsActionMngr* TagsActionMngr::m_defaultManager = nullptr;
66 
defaultManager()67 TagsActionMngr* TagsActionMngr::defaultManager()
68 {
69     return m_defaultManager;
70 }
71 
72 class Q_DECL_HIDDEN TagsActionMngr::Private
73 {
74 public:
75 
Private()76     explicit Private()
77         : ratingShortcutPrefix(QLatin1String("rateshortcut")),
78           tagShortcutPrefix   (QLatin1String("tagshortcut")),
79           pickShortcutPrefix  (QLatin1String("pickshortcut")),
80           colorShortcutPrefix (QLatin1String("colorshortcut"))
81     {
82     }
83 
84     QMultiMap<int, QAction*>  tagsActionMap;
85     QList<KActionCollection*> actionCollectionList;
86 
87     const QString             ratingShortcutPrefix;
88     const QString             tagShortcutPrefix;
89     const QString             pickShortcutPrefix;
90     const QString             colorShortcutPrefix;
91 };
92 
93 // -------------------------------------------------------------------------------------------------
94 
TagsActionMngr(QWidget * const parent)95 TagsActionMngr::TagsActionMngr(QWidget* const parent)
96     : QObject(parent),
97       d      (new Private)
98 {
99     if (!m_defaultManager)
100     {
101         m_defaultManager = this;
102     }
103 
104     connect(AlbumManager::instance(), SIGNAL(signalAlbumDeleted(Album*)),
105             this, SLOT(slotAlbumDeleted(Album*)));
106 }
107 
~TagsActionMngr()108 TagsActionMngr::~TagsActionMngr()
109 {
110     delete d;
111 
112     if (m_defaultManager == this)
113     {
114         m_defaultManager = nullptr;
115     }
116 }
117 
ratingShortcutPrefix() const118 QString TagsActionMngr::ratingShortcutPrefix() const
119 {
120     return d->ratingShortcutPrefix;
121 }
122 
tagShortcutPrefix() const123 QString TagsActionMngr::tagShortcutPrefix() const
124 {
125     return d->tagShortcutPrefix;
126 }
127 
pickShortcutPrefix() const128 QString TagsActionMngr::pickShortcutPrefix() const
129 {
130     return d->pickShortcutPrefix;
131 }
132 
colorShortcutPrefix() const133 QString TagsActionMngr::colorShortcutPrefix() const
134 {
135     return d->colorShortcutPrefix;
136 }
137 
registerTagsActionCollections()138 void TagsActionMngr::registerTagsActionCollections()
139 {
140     d->actionCollectionList.append(DigikamApp::instance()->actionCollection());
141     d->actionCollectionList.append(ImageWindow::imageWindow()->actionCollection());
142     d->actionCollectionList.append(LightTableWindow::lightTableWindow()->actionCollection());
143 
144     // Create Tags shortcuts.
145 
146     QList<int> tagIds = TagsCache::instance()->tagsWithProperty(TagPropertyName::tagKeyboardShortcut());
147 
148     foreach (int tagId, tagIds)
149     {
150         createTagActionShortcut(tagId);
151     }
152 }
153 
actionCollections() const154 QList<KActionCollection*> TagsActionMngr::actionCollections() const
155 {
156     return d->actionCollectionList;
157 }
158 
registerLabelsActions(KActionCollection * const ac)159 void TagsActionMngr::registerLabelsActions(KActionCollection* const ac)
160 {
161     // Create Rating shortcuts.
162 
163     for (int i = RatingMin ; i <= RatingMax ; ++i)
164     {
165         createRatingActionShortcut(ac, i);
166     }
167 
168     // Create Color Label shortcuts.
169 
170     for (int i = NoColorLabel ; i <= WhiteLabel ; ++i)
171     {
172         createColorLabelActionShortcut(ac, i);
173     }
174 
175     // Create Pick Label shortcuts.
176 
177     for (int i = NoPickLabel ; i <= AcceptedLabel ; ++i)
178     {
179         createPickLabelActionShortcut(ac, i);
180     }
181 }
182 
registerActionsToWidget(QWidget * const wdg)183 void TagsActionMngr::registerActionsToWidget(QWidget* const wdg)
184 {
185     DXmlGuiWindow* const win = dynamic_cast<DXmlGuiWindow*>(qApp->activeWindow());
186 
187     if (win)
188     {
189         foreach (QAction* const ac, win->actionCollection()->actions())
190         {
191             if (ac->objectName().startsWith(d->ratingShortcutPrefix) ||
192                 ac->objectName().startsWith(d->tagShortcutPrefix)    ||
193                 ac->objectName().startsWith(d->pickShortcutPrefix)   ||
194                 ac->objectName().startsWith(d->colorShortcutPrefix))
195             {
196                 wdg->addAction(ac);
197             }
198         }
199     }
200 }
201 
createRatingActionShortcut(KActionCollection * const ac,int rating)202 bool TagsActionMngr::createRatingActionShortcut(KActionCollection* const ac, int rating)
203 {
204     if (ac)
205     {
206         QAction* const action = ac->addAction(QString::fromUtf8("%1-%2").arg(d->ratingShortcutPrefix).arg(rating));
207         action->setText(i18n("Assign Rating \"%1 Star\"", rating));
208         ac->setDefaultShortcut(action, QKeySequence(QString::fromUtf8("CTRL+%1").arg(rating)));
209         action->setIcon(RatingWidget::buildIcon(rating, 32));
210         action->setData(rating);
211 
212         connect(action, SIGNAL(triggered()),
213                 this, SLOT(slotAssignFromShortcut()));
214 
215         return true;
216     }
217 
218     return false;
219 }
220 
createPickLabelActionShortcut(KActionCollection * const ac,int pickId)221 bool TagsActionMngr::createPickLabelActionShortcut(KActionCollection* const ac, int pickId)
222 {
223     if (ac)
224     {
225         QAction* const action = ac->addAction(QString::fromUtf8("%1-%2").arg(d->pickShortcutPrefix).arg(pickId));
226         action->setText(i18n("Assign Pick Label \"%1\"", PickLabelWidget::labelPickName((PickLabel)pickId)));
227         ac->setDefaultShortcut(action, QKeySequence(QString::fromUtf8("ALT+%1").arg(pickId)));
228         action->setIcon(PickLabelWidget::buildIcon((PickLabel)pickId));
229         action->setData(pickId);
230 
231         connect(action, SIGNAL(triggered()),
232                 this, SLOT(slotAssignFromShortcut()));
233 
234         return true;
235     }
236 
237     return false;
238 }
239 
createColorLabelActionShortcut(KActionCollection * const ac,int colorId)240 bool TagsActionMngr::createColorLabelActionShortcut(KActionCollection* const ac, int colorId)
241 {
242     if (ac)
243     {
244         QAction* const action = ac->addAction(QString::fromUtf8("%1-%2").arg(d->colorShortcutPrefix).arg(colorId));
245         action->setText(i18n("Assign Color Label \"%1\"", ColorLabelWidget::labelColorName((ColorLabel)colorId)));
246         ac->setDefaultShortcut(action, QKeySequence(QString::fromUtf8("ALT+CTRL+%1").arg(colorId)));
247         action->setIcon(ColorLabelWidget::buildIcon((ColorLabel)colorId, 32));
248         action->setData(colorId);
249 
250         connect(action, SIGNAL(triggered()),
251                 this, SLOT(slotAssignFromShortcut()));
252 
253         return true;
254     }
255 
256     return false;
257 }
258 
createTagActionShortcut(int tagId)259 bool TagsActionMngr::createTagActionShortcut(int tagId)
260 {
261     if (!tagId)
262     {
263         return false;
264     }
265 
266     TAlbum* const talbum = AlbumManager::instance()->findTAlbum(tagId);
267 
268     if (!talbum)
269     {
270         return false;
271     }
272 
273     QString value = TagsCache::instance()->propertyValue(tagId, TagPropertyName::tagKeyboardShortcut());
274 
275     if (value.isEmpty())
276     {
277         return false;
278     }
279 
280     QKeySequence ks(value);
281 
282     // FIXME: tag icons can be files on disk, or system icon names. Only the latter will work here.
283 
284     QIcon     icon(SyncJob::getTagThumbnail(talbum));
285 
286     qCDebug(DIGIKAM_GENERAL_LOG) << "Create Shortcut " << ks.toString()
287                                  << " to Tag " << talbum->title()
288                                  << " (" << tagId << ")";
289 
290     foreach (KActionCollection* const ac, d->actionCollectionList)
291     {
292         QAction* const action = ac->addAction(QString::fromUtf8("%1-%2").arg(d->tagShortcutPrefix).arg(tagId));
293         action->setText(i18n("Assign Tag \"%1\"", talbum->title()));
294         action->setParent(this);
295         ac->setDefaultShortcut(action, ks);
296         action->setIcon(icon);
297         action->setData(tagId);
298 
299         connect(action, SIGNAL(triggered()),
300                 this, SLOT(slotAssignFromShortcut()));
301 
302         connect(action, SIGNAL(changed()),
303                 this, SLOT(slotTagActionChanged()));
304 
305         d->tagsActionMap.insert(tagId, action);
306     }
307 
308     return true;
309 }
310 
slotTagActionChanged()311 void TagsActionMngr::slotTagActionChanged()
312 {
313     QAction* const action = dynamic_cast<QAction*>(sender());
314 
315     if (!action)
316     {
317         return;
318     }
319 
320     int tagId       = action->data().toInt();
321 
322     QKeySequence ks;
323     QStringList lst = action->shortcut().toString().split(QLatin1Char(','));
324 
325     if (!lst.isEmpty())
326     {
327         ks = QKeySequence(lst.first());
328     }
329 
330     updateTagShortcut(tagId, ks, false);
331 }
332 
updateTagShortcut(int tagId,const QKeySequence & ks,bool delAction)333 void TagsActionMngr::updateTagShortcut(int tagId, const QKeySequence& ks, bool delAction)
334 {
335     if (!tagId)
336     {
337         return;
338     }
339 
340     qCDebug(DIGIKAM_GENERAL_LOG) << "Tag Shortcut " << tagId << "Changed to " << ks;
341 
342     QString value = TagsCache::instance()->propertyValue(tagId, TagPropertyName::tagKeyboardShortcut());
343 
344     if (value == ks.toString())
345     {
346         return;
347     }
348 
349     TagProperties tprop(tagId);
350 
351     if (ks.isEmpty())
352     {
353         removeTagActionShortcut(tagId, delAction);
354         tprop.removeProperties(TagPropertyName::tagKeyboardShortcut());
355     }
356     else
357     {
358         removeTagActionShortcut(tagId, delAction);
359         tprop.setProperty(TagPropertyName::tagKeyboardShortcut(), ks.toString());
360         createTagActionShortcut(tagId);
361     }
362 }
363 
slotAlbumDeleted(Album * album)364 void TagsActionMngr::slotAlbumDeleted(Album* album)
365 {
366     TAlbum* const talbum = dynamic_cast<TAlbum*>(album);
367 
368     if (!talbum)
369     {
370         return;
371     }
372 
373     removeTagActionShortcut(talbum->id());
374     qCDebug(DIGIKAM_GENERAL_LOG) << "Delete Shortcut assigned to tag " << album->id();
375 }
376 
removeTagActionShortcut(int tagId,bool delAction)377 bool TagsActionMngr::removeTagActionShortcut(int tagId, bool delAction)
378 {
379     if (!d->tagsActionMap.contains(tagId))
380     {
381         return false;
382     }
383 
384     foreach (QAction* const act, d->tagsActionMap.values(tagId))
385     {
386         if (act)
387         {
388             KActionCollection* const ac = dynamic_cast<KActionCollection*>(act->parent());
389 
390             if (ac)
391             {
392                 ac->takeAction(act);
393             }
394 
395             if (delAction)
396             {
397                 delete act;
398             }
399         }
400     }
401 
402     d->tagsActionMap.remove(tagId);
403 
404     return true;
405 }
406 
slotAssignFromShortcut()407 void TagsActionMngr::slotAssignFromShortcut()
408 {
409     QAction* const action = dynamic_cast<QAction*>(sender());
410 
411     if (!action)
412     {
413         return;
414     }
415 
416     int val               = action->data().toInt();
417     qCDebug(DIGIKAM_GENERAL_LOG) << "Shortcut value: " << val;
418 
419     QWidget* const w      = qApp->activeWindow();
420     DigikamApp* const dkw = dynamic_cast<DigikamApp*>(w);
421 
422     if (dkw)
423     {
424         //qCDebug(DIGIKAM_GENERAL_LOG) << "Handling by DigikamApp";
425 
426         if      (action->objectName().startsWith(d->ratingShortcutPrefix))
427         {
428             dkw->view()->slotAssignRating(val);
429         }
430         else if (action->objectName().startsWith(d->pickShortcutPrefix))
431         {
432             dkw->view()->slotAssignPickLabel(val);
433         }
434         else if (action->objectName().startsWith(d->colorShortcutPrefix))
435         {
436             dkw->view()->slotAssignColorLabel(val);
437         }
438         else if (action->objectName().startsWith(d->tagShortcutPrefix))
439         {
440             dkw->view()->toggleTag(val);
441         }
442 
443         return;
444     }
445 
446     ImageWindow* const imw = dynamic_cast<ImageWindow*>(w);
447 
448     if (imw)
449     {
450         //qCDebug(DIGIKAM_GENERAL_LOG) << "Handling by ImageWindow";
451 
452         if      (action->objectName().startsWith(d->ratingShortcutPrefix))
453         {
454             imw->slotAssignRating(val);
455         }
456         else if (action->objectName().startsWith(d->pickShortcutPrefix))
457         {
458             imw->slotAssignPickLabel(val);
459         }
460         else if (action->objectName().startsWith(d->colorShortcutPrefix))
461         {
462             imw->slotAssignColorLabel(val);
463         }
464         else if (action->objectName().startsWith(d->tagShortcutPrefix))
465         {
466             imw->toggleTag(val);
467         }
468 
469         return;
470     }
471 
472     LightTableWindow* const ltw = dynamic_cast<LightTableWindow*>(w);
473 
474     if (ltw)
475     {
476         //qCDebug(DIGIKAM_GENERAL_LOG) << "Handling by LightTableWindow";
477 
478         if      (action->objectName().startsWith(d->ratingShortcutPrefix))
479         {
480             ltw->slotAssignRating(val);
481         }
482         else if (action->objectName().startsWith(d->pickShortcutPrefix))
483         {
484             ltw->slotAssignPickLabel(val);
485         }
486         else if (action->objectName().startsWith(d->colorShortcutPrefix))
487         {
488             ltw->slotAssignColorLabel(val);
489         }
490         else if (action->objectName().startsWith(d->tagShortcutPrefix))
491         {
492             ltw->toggleTag(val);
493         }
494 
495         return;
496     }
497 
498     // emit signal to DInfoInterface to broadcast to another component:
499 
500     emit signalShortcutPressed(action->objectName(), val);
501 }
502 
503 } // namespace Digikam
504