1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2007-09-19
7 * Description : Access to comments of an item in the database
8 *
9 * Copyright (C) 2007-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10 * Copyright (C) 2009-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11 *
12 * This program is free software; you can redistribute it
13 * and/or modify it under the terms of the GNU General
14 * Public License as published by the Free Software Foundation;
15 * either version 2, or (at your option)
16 * any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * ============================================================ */
24
25 #include "itemcomments.h"
26
27 // Qt includes
28
29 #include <QLocale>
30
31 // Local includes
32
33 #include "coredb.h"
34
35 namespace Digikam
36 {
37
38 class Q_DECL_HIDDEN ItemComments::Private : public QSharedData
39 {
40 public:
41
Private()42 explicit Private()
43 : id (-1),
44 unique(ItemComments::UniquePerLanguage)
45 {
46 }
47
init(const CoreDbAccess & access,qlonglong imageId)48 void init(const CoreDbAccess& access, qlonglong imageId)
49 {
50 id = imageId;
51 infos = access.db()->getItemComments(id);
52
53 for (int i = 0 ; i < infos.size() ; ++i)
54 {
55 CommentInfo& info = infos[i];
56
57 if (info.language.isNull())
58 {
59 info.language = QLatin1String("x-default");
60 }
61 }
62 }
63
languageMatch(const QString & fullCode,const QString & langCode,int & fullCodeMatch,int & langCodeMatch,int & defaultCodeMatch,int & firstMatch,DatabaseComment::Type type=DatabaseComment::Comment) const64 void languageMatch(const QString& fullCode,
65 const QString& langCode,
66 int& fullCodeMatch,
67 int& langCodeMatch,
68 int& defaultCodeMatch,
69 int& firstMatch,
70 DatabaseComment::Type type = DatabaseComment::Comment) const
71 {
72 // if you change the algorithm, please take a look at ItemCopyright as well
73
74 fullCodeMatch = -1;
75 langCodeMatch = -1;
76 defaultCodeMatch = -1;
77 firstMatch = -1;
78
79 if (infos.isEmpty())
80 {
81 return;
82 }
83
84 // First we search for a full match
85 // Second for a match of the language code
86 // Third for the default code
87 // Fourth we return the first comment
88
89 QLatin1String defaultCode("x-default");
90
91 for (int i = 0 ; i < infos.size() ; ++i)
92 {
93 const CommentInfo& info = infos.at(i);
94
95 if (info.type == type)
96 {
97 if (firstMatch == -1)
98 {
99 firstMatch = i;
100 }
101
102 if (info.language == fullCode)
103 {
104 fullCodeMatch = i;
105 break;
106 }
107 else if (info.language.startsWith(langCode) && langCodeMatch == -1)
108 {
109 langCodeMatch = i;
110 }
111 else if (info.language == defaultCode)
112 {
113 defaultCodeMatch = i;
114 }
115 }
116 }
117 }
118
adjustStoredIndexes(QSet<int> & set,int removedIndex)119 void adjustStoredIndexes(QSet<int>& set, int removedIndex)
120 {
121 QSet<int> newSet;
122
123 foreach (int index, set)
124 {
125 if (index > removedIndex)
126 {
127 newSet << index - 1;
128 }
129 else if (index < removedIndex)
130 {
131 newSet << index;
132 }
133
134 // drop index == removedIndex
135 }
136
137 set = newSet;
138 }
139
adjustStoredIndexes(int removedIndex)140 void adjustStoredIndexes(int removedIndex)
141 {
142 adjustStoredIndexes(dirtyIndices, removedIndex);
143 adjustStoredIndexes(newIndices, removedIndex);
144 }
145
146 public:
147
148 qlonglong id;
149 QList<CommentInfo> infos;
150 QSet<int> dirtyIndices;
151 QSet<int> newIndices;
152 QSet<int> idsToRemove;
153 ItemComments::UniqueBehavior unique;
154 };
155
ItemComments()156 ItemComments::ItemComments()
157 : d(nullptr)
158 {
159 }
160
ItemComments(qlonglong imageid)161 ItemComments::ItemComments(qlonglong imageid)
162 : d(new Private)
163 {
164 CoreDbAccess access;
165 d->init(access, imageid);
166 }
167
ItemComments(const CoreDbAccess & access,qlonglong imageid)168 ItemComments::ItemComments(const CoreDbAccess& access, qlonglong imageid)
169 : d(new Private)
170 {
171 d->init(access, imageid);
172 }
173
ItemComments(const ItemComments & other)174 ItemComments::ItemComments(const ItemComments& other)
175 : d(other.d)
176 {
177 }
178
~ItemComments()179 ItemComments::~ItemComments()
180 {
181 apply();
182 }
183
operator =(const ItemComments & other)184 ItemComments& ItemComments::operator=(const ItemComments& other)
185 {
186 d = other.d;
187
188 return *this;
189 }
190
isNull() const191 bool ItemComments::isNull() const
192 {
193 return !d;
194 }
195
defaultComment(DatabaseComment::Type type) const196 QString ItemComments::defaultComment(DatabaseComment::Type type) const
197 {
198 return defaultComment(nullptr, type);
199 }
200
defaultComment(int * const index,DatabaseComment::Type type) const201 QString ItemComments::defaultComment(int* const index, DatabaseComment::Type type) const
202 {
203 if (!d)
204 {
205 return QString();
206 }
207
208 QString spec = QLocale().name().toLower();
209 QString langCode = spec.left(spec.indexOf(QLatin1Char('_'))) + QLatin1Char('-');
210 QString fullCode = spec.replace(QLatin1Char('_'), QLatin1Char('-'));
211
212 int fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch;
213
214 d->languageMatch(fullCode, langCode, fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch, type);
215
216 int chosen = fullCodeMatch;
217
218 if (chosen == -1)
219 {
220 chosen = langCodeMatch;
221 }
222
223 if (chosen == -1)
224 {
225 chosen = defaultCodeMatch;
226 }
227
228 if (chosen == -1)
229 {
230 chosen = firstMatch;
231 }
232
233 if (index)
234 {
235 *index = chosen;
236 }
237
238 if (chosen == -1)
239 {
240 return QString();
241 }
242 else
243 {
244 return d->infos.at(chosen).comment;
245 }
246 }
247
commentForLanguage(const QString & languageCode,int * const index,LanguageChoiceBehavior behavior) const248 QString ItemComments::commentForLanguage(const QString& languageCode,
249 int* const index,
250 LanguageChoiceBehavior behavior) const
251 {
252 if (!d)
253 {
254 return QString();
255 }
256
257 int fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch;
258
259 // en-us => en-
260
261 QString firstPart;
262
263 if (languageCode == QLatin1String("x-default"))
264 {
265 firstPart = languageCode;
266 }
267 else
268 {
269 firstPart = languageCode.section(QLatin1Char('-'), 0, 0, QString::SectionIncludeTrailingSep);
270 }
271
272 d->languageMatch(languageCode, firstPart, fullCodeMatch, langCodeMatch, defaultCodeMatch, firstMatch);
273
274 int chosen = fullCodeMatch;
275
276 if (chosen == -1)
277 {
278 chosen = langCodeMatch;
279 }
280
281 if ((chosen == -1) && (behavior > ReturnMatchingLanguageOnly))
282 {
283 chosen = defaultCodeMatch;
284
285 if ((chosen == -1) && (behavior == ReturnMatchingDefaultOrFirstLanguage))
286 {
287 chosen = firstMatch;
288 }
289 }
290
291 if (index)
292 {
293 *index = chosen;
294 }
295
296 if (chosen == -1)
297 {
298 return QString();
299 }
300 else
301 {
302 return d->infos.at(chosen).comment;
303 }
304 }
305
numberOfComments() const306 int ItemComments::numberOfComments() const
307 {
308 if (!d)
309 {
310 return 0;
311 }
312
313 return d->infos.size();
314 }
315
type(int index) const316 DatabaseComment::Type ItemComments::type(int index) const
317 {
318 if (!d)
319 {
320 return DatabaseComment::UndefinedType;
321 }
322
323 return d->infos.at(index).type;
324 }
325
language(int index) const326 QString ItemComments::language(int index) const
327 {
328 if (!d)
329 {
330 return QString();
331 }
332
333 return d->infos.at(index).language;
334 }
335
author(int index) const336 QString ItemComments::author(int index) const
337 {
338 if (!d)
339 {
340 return QString();
341 }
342
343 return d->infos.at(index).author;
344 }
345
date(int index) const346 QDateTime ItemComments::date(int index) const
347 {
348 if (!d)
349 {
350 return QDateTime();
351 }
352
353 return d->infos.at(index).date;
354 }
355
comment(int index) const356 QString ItemComments::comment(int index) const
357 {
358 if (!d)
359 {
360 return QString();
361 }
362
363 return d->infos.at(index).comment;
364 }
365
setUniqueBehavior(UniqueBehavior behavior)366 void ItemComments::setUniqueBehavior(UniqueBehavior behavior)
367 {
368 if (!d)
369 {
370 return;
371 }
372
373 d->unique = behavior;
374 }
375
addComment(const QString & comment,const QString & lang,const QString & author_,const QDateTime & date,DatabaseComment::Type type)376 void ItemComments::addComment(const QString& comment,
377 const QString& lang,
378 const QString& author_,
379 const QDateTime& date,
380 DatabaseComment::Type type)
381 {
382 if (!d)
383 {
384 return;
385 }
386
387 bool multipleCommentsPerLanguage = (d->unique == UniquePerLanguageAndAuthor);
388 QString language = lang;
389
390 if (language.isEmpty())
391 {
392 language = QLatin1String("x-default");
393 }
394
395 QString author = author_;
396
397 /// @todo This makes no sense - is another variable supposed to be used instead? - Michael Hansen
398
399 if (author.isEmpty())
400 {
401 author = QString();
402 }
403
404 for (int i = 0 ; i < d->infos.size() ; ++i)
405 {
406 CommentInfo& info = d->infos[i];
407
408 // some extra considerations on replacing
409
410 if ((info.type == type) && (info.type == DatabaseComment::Comment) && (info.language == language))
411 {
412 if (!multipleCommentsPerLanguage || (info.author == author))
413 {
414 info.comment = comment;
415 info.date = date;
416 info.author = author;
417 d->dirtyIndices << i;
418 return;
419 }
420 }
421
422 // simulate unique restrictions of db.
423 // There is a problem that a NULL value is never unique, see #189080
424
425 if ((info.type == type) &&
426 (info.language == language) &&
427 ((info.author == author) || (info.author.isEmpty() && author.isEmpty())))
428 {
429 info.comment = comment;
430 info.date = date;
431 d->dirtyIndices << i;
432 return;
433 }
434 }
435
436 addCommentDirectly(comment, language, author, type, date);
437 }
438
addHeadline(const QString & headline,const QString & lang,const QString & author,const QDateTime & date)439 void ItemComments::addHeadline(const QString& headline, const QString& lang,
440 const QString& author, const QDateTime& date)
441 {
442 addComment(headline, lang, author, date, DatabaseComment::Headline);
443 }
444
addTitle(const QString & title,const QString & lang,const QString & author,const QDateTime & date)445 void ItemComments::addTitle(const QString& title, const QString& lang,
446 const QString& author, const QDateTime& date)
447 {
448 addComment(title, lang, author, date, DatabaseComment::Title);
449 }
450
replaceComments(const CaptionsMap & map,DatabaseComment::Type type)451 void ItemComments::replaceComments(const CaptionsMap& map, DatabaseComment::Type type)
452 {
453 if (!d)
454 {
455 return;
456 }
457
458 d->dirtyIndices.clear();
459
460 for (CaptionsMap::const_iterator it = map.constBegin() ; it != map.constEnd() ; ++it)
461 {
462 CaptionValues val = it.value();
463 addComment(val.caption, it.key(), val.author, val.date, type);
464 }
465
466 // remove all comments of this type that have not been touched above
467
468 for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
469 {
470 if (!d->dirtyIndices.contains(i) && !d->newIndices.contains(i) && (d->infos[i].type == type))
471 {
472 remove(i);
473 }
474 else
475 {
476 ++i;
477 }
478 }
479 }
480
replaceFrom(const ItemComments & source)481 void ItemComments::replaceFrom(const ItemComments& source)
482 {
483 if (!d)
484 {
485 return;
486 }
487
488 if (!source.d)
489 {
490 removeAll();
491 return;
492 }
493
494 foreach (const CommentInfo& info, source.d->infos)
495 {
496 addComment(info.comment, info.language, info.author, info.date, info.type);
497 }
498
499 // remove all that have not been touched above
500
501 for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
502 {
503 if (!d->dirtyIndices.contains(i) && !d->newIndices.contains(i))
504 {
505 remove(i);
506 }
507 else
508 {
509 ++i;
510 }
511 }
512 }
513
addCommentDirectly(const QString & comment,const QString & language,const QString & author,DatabaseComment::Type type,const QDateTime & date)514 void ItemComments::addCommentDirectly(const QString& comment,
515 const QString& language,
516 const QString& author,
517 DatabaseComment::Type type,
518 const QDateTime& date)
519 {
520 CommentInfo info;
521 info.comment = comment;
522 info.language = language;
523 info.author = author;
524 info.type = type;
525 info.date = date;
526
527 d->newIndices << d->infos.size();
528 d->infos << info;
529 }
530
remove(int index)531 void ItemComments::remove(int index)
532 {
533 if (!d)
534 {
535 return;
536 }
537
538 d->idsToRemove << d->infos.at(index).id;
539 d->infos.removeAt(index);
540 d->adjustStoredIndexes(index);
541 }
542
removeAll(DatabaseComment::Type type)543 void ItemComments::removeAll(DatabaseComment::Type type)
544 {
545 if (!d)
546 {
547 return;
548 }
549
550 for (int i = 0 ; i < d->infos.size() /* changing! */ ; )
551 {
552 if (d->infos.at(i).type == type)
553 {
554 remove(i);
555 }
556 else
557 {
558 ++i;
559 }
560 }
561 }
562
removeAllComments()563 void ItemComments::removeAllComments()
564 {
565 removeAll(DatabaseComment::Comment);
566 }
567
removeAll()568 void ItemComments::removeAll()
569 {
570 if (!d)
571 {
572 return;
573 }
574
575 foreach (const CommentInfo& info, d->infos)
576 {
577 d->idsToRemove << info.id;
578 }
579
580 d->infos.clear();
581 d->dirtyIndices.clear();
582 d->newIndices.clear();
583 }
584
changeComment(int index,const QString & comment)585 void ItemComments::changeComment(int index, const QString& comment)
586 {
587 if (!d)
588 {
589 return;
590 }
591
592 d->infos[index].comment = comment;
593 d->dirtyIndices << index;
594 }
595
changeLanguage(int index,const QString & language)596 void ItemComments::changeLanguage(int index, const QString& language)
597 {
598 if (!d)
599 {
600 return;
601 }
602
603 d->infos[index].language = language;
604 d->dirtyIndices << index;
605 }
606
changeAuthor(int index,const QString & author)607 void ItemComments::changeAuthor(int index, const QString& author)
608 {
609 if (!d)
610 {
611 return;
612 }
613
614 d->infos[index].author = author;
615 d->dirtyIndices << index;
616 }
617
changeDate(int index,const QDateTime & date)618 void ItemComments::changeDate(int index, const QDateTime& date)
619 {
620 if (!d)
621 {
622 return;
623 }
624
625 d->infos[index].date = date;
626 d->dirtyIndices << index;
627 }
628
changeType(int index,DatabaseComment::Type type)629 void ItemComments::changeType(int index, DatabaseComment::Type type)
630 {
631 if (!d)
632 {
633 return;
634 }
635
636 d->infos[index].type = type;
637 d->dirtyIndices << index;
638 }
639
apply()640 void ItemComments::apply()
641 {
642 if (!d)
643 {
644 return;
645 }
646
647 CoreDbAccess access;
648 apply(access);
649 }
650
apply(CoreDbAccess & access)651 void ItemComments::apply(CoreDbAccess& access)
652 {
653 if (!d)
654 {
655 return;
656 }
657
658 foreach (int commentId, d->idsToRemove)
659 {
660 access.db()->removeImageComment(commentId, d->id);
661 }
662
663 d->idsToRemove.clear();
664
665 foreach (int index, d->newIndices)
666 {
667 CommentInfo& info = d->infos[index];
668 info.id = access.db()->setImageComment(d->id, info.comment, info.type, info.language, info.author, info.date);
669 }
670
671 d->dirtyIndices.subtract(d->newIndices);
672 d->newIndices.clear();
673
674 foreach (int index, d->dirtyIndices)
675 {
676 QVariantList values;
677 CommentInfo& info = d->infos[index];
678 values << (int)info.type << info.language << info.author << info.date << info.comment;
679 access.db()->changeImageComment(info.id, d->id, values);
680 }
681
682 d->dirtyIndices.clear();
683 }
684
toCaptionsMap(DatabaseComment::Type type) const685 CaptionsMap ItemComments::toCaptionsMap(DatabaseComment::Type type) const
686 {
687 CaptionsMap map;
688
689 if (d)
690 {
691 foreach (const CommentInfo& info, d->infos)
692 {
693 if (info.type == type)
694 {
695 CaptionValues val;
696 val.caption = info.comment;
697 val.author = info.author;
698 val.date = info.date;
699 map[info.language] = val;
700 }
701 }
702 }
703
704 return map;
705 }
706
707 } // namespace Digikam
708