diff --git a/rtdata/languages/default b/rtdata/languages/default index 88e2f68f2..ad8962037 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -2090,6 +2090,7 @@ PREFERENCES_THUMBNAIL_INSPECTOR_JPEG;Embedded JPEG preview PREFERENCES_THUMBNAIL_INSPECTOR_MODE;Image to show PREFERENCES_THUMBNAIL_INSPECTOR_RAW;Neutral raw rendering PREFERENCES_THUMBNAIL_INSPECTOR_RAW_IF_NO_JPEG_FULLSIZE;Embedded JPEG if fullsize, neutral raw otherwise +PREFERENCES_THUMBNAIL_RANK_COLOR_MODE;Load/Save thumbnail rank and color from/to XMP sidecars PREFERENCES_TOOLPANEL_AVAILABLETOOLS;Available Tools PREFERENCES_TOOLPANEL_CLONE_FAVORITES;Keep favorite tools in original locations PREFERENCES_TOOLPANEL_CLONE_FAVORITES_TOOLTIP;If set, favorite tools will appear in both the favorites tab and their original tabs.\n\nNote: Enabling this option may result in a slight delay when switching tabs. diff --git a/rtengine/metadata.cc b/rtengine/metadata.cc index 0a55c1424..df8988f5a 100644 --- a/rtengine/metadata.cc +++ b/rtengine/metadata.cc @@ -106,19 +106,6 @@ void clear_metadata_key(Data &data, const Key &key) } } -template -auto to_long(const Iterator &iter, Integer n = Integer{0}) -> decltype( -#if EXIV2_TEST_VERSION(0,28,0) - iter->toInt64() -) { - return iter->toInt64(n); -#else - iter->toLong() -) { - return iter->toLong(n); -#endif -} - } // namespace diff --git a/rtengine/metadata.h b/rtengine/metadata.h index 7424b2720..75d37d46c 100644 --- a/rtengine/metadata.h +++ b/rtengine/metadata.h @@ -97,4 +97,18 @@ private: static std::unique_ptr cache_; }; +template +auto to_long(const Iterator &iter, Integer n = Integer{0}) -> decltype( +#if EXIV2_TEST_VERSION(0,28,0) + iter->toInt64() +) { + return iter->toInt64(n); +#else + iter->toLong() +) { + return iter->toLong(n); +#endif +} + + } // namespace rtengine diff --git a/rtgui/filebrowser.cc b/rtgui/filebrowser.cc index 26f0d5cd6..661b6051a 100644 --- a/rtgui/filebrowser.cc +++ b/rtgui/filebrowser.cc @@ -547,13 +547,13 @@ void FileBrowser::rightClicked () untrash->set_sensitive (false); for (size_t i = 0; i < selected.size(); i++) - if ((static_cast(selected[i]))->thumbnail->getStage()) { + if ((static_cast(selected[i]))->thumbnail->getTrashed()) { untrash->set_sensitive (true); break; } for (size_t i = 0; i < selected.size(); i++) - if (!(static_cast(selected[i]))->thumbnail->getStage()) { + if (!(static_cast(selected[i]))->thumbnail->getTrashed()) { trash->set_sensitive (true); break; } @@ -649,7 +649,7 @@ void FileBrowser::addEntry_ (FileBrowserEntry* entry) entry->addButtonSet(new FileThumbnailButtonSet(entry)); entry->getThumbButtonSet()->setRank(entry->thumbnail->getRank()); entry->getThumbButtonSet()->setColorLabel(entry->thumbnail->getColorLabel()); - entry->getThumbButtonSet()->setInTrash(entry->thumbnail->getStage()); + entry->getThumbButtonSet()->setInTrash(entry->thumbnail->getTrashed()); entry->getThumbButtonSet()->setButtonListener(this); entry->resize(getThumbnailHeight()); entry->filtered = !checkFilter(entry); @@ -1023,12 +1023,12 @@ void FileBrowser::menuItemActivated (Gtk::MenuItem* m) const auto thumbnail = mselected[i]->thumbnail; const auto rank = thumbnail->getRank(); const auto colorLabel = thumbnail->getColorLabel(); - const auto stage = thumbnail->getStage(); + const auto stage = thumbnail->getTrashed(); thumbnail->createProcParamsForUpdate (false, true); thumbnail->setRank(rank); thumbnail->setColorLabel(colorLabel); - thumbnail->setStage(stage); + thumbnail->setTrashed(stage); // Empty run to update the thumb rtengine::procparams::ProcParams params = thumbnail->getProcParams (); @@ -1558,8 +1558,8 @@ bool FileBrowser::checkFilter (ThumbBrowserEntryBase* entryb) const // true -> ((entry->thumbnail->isRecentlySaved() && filter.showRecentlySaved[0]) && !filter.showRecentlySaved[1]) || ((!entry->thumbnail->isRecentlySaved() && filter.showRecentlySaved[1]) && !filter.showRecentlySaved[0]) || - (entry->thumbnail->getStage() && !filter.showTrash) || - (!entry->thumbnail->getStage() && !filter.showNotTrash)) { + (entry->thumbnail->getTrashed() && !filter.showTrash) || + (!entry->thumbnail->getTrashed() && !filter.showNotTrash)) { return false; } @@ -1625,11 +1625,11 @@ void FileBrowser::toTrashRequested (std::vector tbe) // no need to notify listeners as item goes to trash, likely to be deleted - if (tbe[i]->thumbnail->getStage()) { + if (tbe[i]->thumbnail->getTrashed()) { continue; } - tbe[i]->thumbnail->setStage (true); + tbe[i]->thumbnail->setTrashed (true); if (tbe[i]->getThumbButtonSet()) { tbe[i]->getThumbButtonSet()->setRank (tbe[i]->thumbnail->getRank()); @@ -1649,11 +1649,11 @@ void FileBrowser::fromTrashRequested (std::vector tbe) for (size_t i = 0; i < tbe.size(); i++) { // if thumbnail was marked inTrash=true then param file must be there, no need to run customprofilebuilder - if (!tbe[i]->thumbnail->getStage()) { + if (!tbe[i]->thumbnail->getTrashed()) { continue; } - tbe[i]->thumbnail->setStage (false); + tbe[i]->thumbnail->setTrashed (false); if (tbe[i]->getThumbButtonSet()) { tbe[i]->getThumbButtonSet()->setRank (tbe[i]->thumbnail->getRank()); @@ -1784,7 +1784,7 @@ void FileBrowser::buttonPressed (LWButton* button, int actionCode, void* actionD FileBrowserEntry* entry = static_cast(actionData); tbe.push_back (entry); - if (!entry->thumbnail->getStage()) { + if (!entry->thumbnail->getTrashed()) { toTrashRequested (tbe); } else { fromTrashRequested (tbe); diff --git a/rtgui/filecatalog.cc b/rtgui/filecatalog.cc index 69b9e6dc9..8c5a70628 100644 --- a/rtgui/filecatalog.cc +++ b/rtgui/filecatalog.cc @@ -1910,7 +1910,7 @@ void FileCatalog::emptyTrash () std::vector toDel; for (const auto entry : t) { - if ((static_cast(entry))->thumbnail->getStage() == 1) { + if ((static_cast(entry))->thumbnail->getTrashed()) { toDel.push_back(static_cast(entry)); } } @@ -1926,7 +1926,7 @@ bool FileCatalog::trashIsEmpty () const auto& t = fileBrowser->getEntries(); for (const auto entry : t) { - if ((static_cast(entry))->thumbnail->getStage() == 1) { + if ((static_cast(entry))->thumbnail->getTrashed()) { return false; } } diff --git a/rtgui/options.cc b/rtgui/options.cc index 8bf431bc5..81bcac3f1 100644 --- a/rtgui/options.cc +++ b/rtgui/options.cc @@ -695,6 +695,7 @@ void Options::setDefaults() lastICCProfCreatorDir = ""; gimpPluginShowInfoDialog = true; maxRecentFolders = 15; + thumbnailRankColorMode = Options::ThumbnailPropertyMode::PROCPARAMS; sortMethod = SORT_BY_NAME; sortDescending = false; rtSettings.lensfunDbDirectory = ""; // set also in main.cc and main-cli.cc @@ -1367,6 +1368,15 @@ void Options::readFromFile(Glib::ustring fname) if (keyFile.has_key("File Browser", "BrowseRecursiveFollowLinks")) { browseRecursiveFollowLinks = keyFile.get_boolean("File Browser", "BrowseRecursiveFollowLinks"); } + + if (keyFile.has_key("File Browser", "ThumbnailRankColorMode")) { + std::string val = keyFile.get_string("File Browser", "ThumbnailRankColorMode"); + if (val == "xmp") { + thumbnailRankColorMode = ThumbnailPropertyMode::XMP; + } else { + thumbnailRankColorMode = ThumbnailPropertyMode::PROCPARAMS; + } + } } if (keyFile.has_group("Clipping Indication")) { @@ -2467,6 +2477,14 @@ void Options::saveToFile(Glib::ustring fname) keyFile.set_string_list("File Browser", "RecentFolders", temp); } + switch (thumbnailRankColorMode) { + case ThumbnailPropertyMode::XMP: + keyFile.set_string("File Browser", "ThumbnailRankColorMode", "xmp"); + break; + default: // ThumbnailPropertyMode::PROCPARAMS + keyFile.set_string("File Browser", "ThumbnailRankColorMode", "procparams"); + break; + } keyFile.set_integer("File Browser", "SortMethod", sortMethod); keyFile.set_boolean("File Browser", "SortDescending", sortDescending); keyFile.set_boolean("File Browser", "BrowseRecursive", browseRecursive); diff --git a/rtgui/options.h b/rtgui/options.h index 2b5df3aa6..d65013dac 100644 --- a/rtgui/options.h +++ b/rtgui/options.h @@ -492,6 +492,12 @@ public: size_t maxRecentFolders; // max. number of recent folders stored in options file std::vector recentFolders; // List containing all recent folders + enum class ThumbnailPropertyMode { + PROCPARAMS, // store rank and color in procparams sidecars + XMP // store rank and color xmp sidecar + }; + ThumbnailPropertyMode thumbnailRankColorMode; + enum SortMethod { SORT_BY_NAME, SORT_BY_DATE, diff --git a/rtgui/preferences.cc b/rtgui/preferences.cc index 527800cb6..eddfc561e 100644 --- a/rtgui/preferences.cc +++ b/rtgui/preferences.cc @@ -1470,6 +1470,9 @@ Gtk::Widget* Preferences::getFileBrowserPanel() vbro->pack_start(*sameThumbSize, Gtk::PACK_SHRINK, 0); vbro->pack_start(*ckbInternalThumbIfUntouched, Gtk::PACK_SHRINK, 0); + thumbnailRankColorMode = Gtk::manage(new Gtk::CheckButton(M("PREFERENCES_THUMBNAIL_RANK_COLOR_MODE"))); + vbro->pack_start(*thumbnailRankColorMode, Gtk::PACK_SHRINK, 0); + Gtk::Box* hbrecent = Gtk::manage(new Gtk::Box()); Gtk::Label* labrecent = Gtk::manage (new Gtk::Label (M("PREFERENCES_MAXRECENTFOLDERS") + ":", Gtk::ALIGN_START)); maxRecentFolders = Gtk::manage(new Gtk::SpinButton()); @@ -2029,6 +2032,8 @@ void Preferences::storePreferences() moptions.rtSettings.metadata_xmp_sync = rtengine::Settings::MetadataXmpSync(metadataSyncCombo->get_active_row_number()); moptions.rtSettings.xmp_sidecar_style = rtengine::Settings::XmpSidecarStyle(xmpSidecarCombo->get_active_row_number()); + + moptions.thumbnailRankColorMode = thumbnailRankColorMode->get_active() ? Options::ThumbnailPropertyMode::XMP : Options::ThumbnailPropertyMode::PROCPARAMS; } void Preferences::fillPreferences() @@ -2287,6 +2292,8 @@ void Preferences::fillPreferences() metadataSyncCombo->set_active(int(moptions.rtSettings.metadata_xmp_sync)); xmpSidecarCombo->set_active(int(moptions.rtSettings.xmp_sidecar_style)); + + thumbnailRankColorMode->set_active(moptions.thumbnailRankColorMode == Options::ThumbnailPropertyMode::XMP); } /* diff --git a/rtgui/preferences.h b/rtgui/preferences.h index b34a8348f..191f122f5 100644 --- a/rtgui/preferences.h +++ b/rtgui/preferences.h @@ -224,6 +224,8 @@ class Preferences final : Gtk::CheckButton* ckbInternalThumbIfUntouched; + Gtk::CheckButton *thumbnailRankColorMode; + Gtk::Entry* txtCustProfBuilderPath; Gtk::ComboBoxText* custProfBuilderLabelType; diff --git a/rtgui/thumbnail.cc b/rtgui/thumbnail.cc index 266dbacd3..7ee4d3da1 100644 --- a/rtgui/thumbnail.cc +++ b/rtgui/thumbnail.cc @@ -105,6 +105,46 @@ bool CPBDump( return true; } +struct ColorMapper { + std::map indexLabelMap; + std::map labelIndexMap; + + ColorMapper(std::map colors) { + for (const auto& color: colors) { + indexLabelMap.insert({color.first, color.second}); + labelIndexMap.insert({color.second, color.first}); + } + } + + int index(const std::string &label) const + { + auto it = labelIndexMap.find(label); + if (it != labelIndexMap.end()) { + return it->second; + } + return 0; + } + + std::string label(int index) const + { + auto it = indexLabelMap.find(index); + if (it != indexLabelMap.end()) { + return it->second; + } + return ""; + } +}; + +const std::map defaultColors = { + {1, "Red"}, + {2, "Yellow"}, + {3, "Green"}, + {4, "Blue"}, + {5, "Purple"} +}; + +auto defaultColorMapper = ColorMapper(defaultColors); + } // namespace using namespace rtengine::procparams; @@ -133,7 +173,7 @@ Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, CacheImageDat generateExifDateTimeStrings (); if (cfs.rankOld >= 0) { - // rank and inTrash were found in cache (old style), move them over to pparams + // rank and inTrash were found in cache (old style), move them over to pparams or xmp sidecar // try to load the last saved parameters from the cache or from the paramfile file createProcParamsForUpdate(false, false); // this can execute customprofilebuilder to generate param file @@ -141,9 +181,11 @@ Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, CacheImageDat // TODO? should we call notifylisterners_procParamsChanged here? setRank(cfs.rankOld); - setStage(cfs.inTrashOld); + setTrashed(cfs.inTrashOld); } + loadProperties(); + delete tpp; tpp = nullptr; } @@ -173,6 +215,8 @@ Thumbnail::Thumbnail(CacheManager* cm, const Glib::ustring& fname, const std::st initial_ = false; + loadProperties(); + delete tpp; tpp = nullptr; } @@ -306,7 +350,7 @@ const ProcParams& Thumbnail::getProcParamsU () * @param returnParams Ask to return a pointer to a ProcParams object if true * @param force True if the profile has to be re-generated even if it already exists * @param flaggingMode True if the ProcParams will be created because the file browser is being flagging an image - * (rang, to trash, color labels). This parameter is passed to the CPB. + * (rank, to trash, color labels). This parameter is passed to the CPB. * * @return Return a pointer to a ProcPamas structure to be updated if returnParams is true and if everything went fine, NULL otherwise. */ @@ -441,12 +485,6 @@ void Thumbnail::clearProcParams (int whoClearedIt) { MyMutex::MyLock lock(mutex); - // preserve rank, colorlabel and inTrash across clear - int rank = getRank(); - int colorlabel = getColorLabel(); - int inTrash = getStage(); - - cfs.recentlySaved = false; pparamsValid = false; @@ -456,13 +494,10 @@ void Thumbnail::clearProcParams (int whoClearedIt) // reset the params to defaults pparams->setDefaults(); - // and restore rank and inTrash - setRank(rank); - pparamsValid = cfs.rating != rank; - setColorLabel(colorlabel); - setStage(inTrash); + // preserve rank, colorlabel and inTrash across clear + updateProcParamsProperties(); - // params could get validated by rank/inTrash values restored above + // params could get validated by updateProcParamsProperties if (pparamsValid) { updateCache(); } else { @@ -560,11 +595,6 @@ void Thumbnail::setProcParams (const ProcParams& pp, ParamsEdited* pe, int whoCh return; } - // do not update rank, colorlabel and inTrash - const int rank = getRank(); - const int colorlabel = getColorLabel(); - const int inTrash = getStage(); - if (pe) { pe->combine(*pparams, pp, true); } else { @@ -573,9 +603,8 @@ void Thumbnail::setProcParams (const ProcParams& pp, ParamsEdited* pe, int whoCh pparamsValid = true; - setRank(rank); - setColorLabel(colorlabel); - setStage(inTrash); + // do not update rank, colorlabel and inTrash + updateProcParamsProperties(); if (updateCacheNow) { updateCache(); @@ -1045,6 +1074,7 @@ void Thumbnail::saveThumbnail () */ void Thumbnail::updateCache (bool updatePParams, bool updateCacheImageData) { + updateProcParamsProperties(); if (updatePParams && pparamsValid) { pparams->save ( @@ -1061,6 +1091,8 @@ void Thumbnail::updateCache (bool updatePParams, bool updateCacheImageData) if (updatePParams && pparamsValid) { saveMetadata(); } + + saveXMPSidecarProperties(); } Thumbnail::~Thumbnail () @@ -1084,48 +1116,34 @@ void Thumbnail::setFileName (const Glib::ustring &fn) cfs.md5 = ::getMD5 (fname); } -int Thumbnail::getRank () const +int Thumbnail::getRank() const { - // prefer the user-set rank over the embedded Rating - // pparams->rank == -1 means that there is no saved rank yet, so we should - // next look for the embedded Rating metadata. - if (pparams->rank != -1) { - return pparams->rank; - } else { - return cfs.rating; - } + return properties.rank; } -void Thumbnail::setRank (int rank) +void Thumbnail::setRank(int rank) { - pparams->rank = rank; - pparamsValid = true; + properties.rank = rank; } -int Thumbnail::getColorLabel () const +int Thumbnail::getColorLabel() const { - return pparams->colorlabel; + return properties.color; } -void Thumbnail::setColorLabel (int colorlabel) +void Thumbnail::setColorLabel(int colorlabel) { - if (pparams->colorlabel != colorlabel) { - pparams->colorlabel = colorlabel; - pparamsValid = true; - } + properties.color = colorlabel; } -int Thumbnail::getStage () const +bool Thumbnail::getTrashed() const { - return pparams->inTrash; + return properties.trashed; } -void Thumbnail::setStage (bool stage) +void Thumbnail::setTrashed(bool trashed) { - if (pparams->inTrash != stage) { - pparams->inTrash = stage; - pparamsValid = true; - } + properties.trashed = trashed; } void Thumbnail::addThumbnailListener (ThumbnailListener* tnl) @@ -1239,6 +1257,107 @@ void Thumbnail::getCamWB(double& temp, double& green, rtengine::StandardObserver } } +void Thumbnail::loadProperties() +{ + properties = Properties(); + + // get initial rank from cache or image metadata + if (cfs.exifValid) { + properties.rank.value = rtengine::LIM(cfs.getRating(), 0, 5); + } else { + const std::unique_ptr md(rtengine::FramesMetaData::fromFile(fname)); + if (md && md->hasExif()) { + properties.rank.value = rtengine::LIM(md->getRating(), 0, 5); + } + } + + // update rank and color from procparams or xmp sidecar + // load trash from procparams + if (pparamsValid) { + if (options.thumbnailRankColorMode == Options::ThumbnailPropertyMode::PROCPARAMS) { + if (pparams->rank >= 0) { + properties.rank.value = pparams->rank; + } + } + + properties.trashed.value = pparams->inTrash; + properties.color.value = pparams->colorlabel; + } + + if (options.thumbnailRankColorMode == Options::ThumbnailPropertyMode::XMP) { + try { + auto xmp = rtengine::Exiv2Metadata::getXmpSidecar(fname); + auto pos = xmp.findKey(Exiv2::XmpKey("Xmp.xmp.Rating")); + if (pos != xmp.end()) { + int r = rtengine::to_long(pos); + properties.rank.value = rtengine::LIM(r, 0, 5); + } + + pos = xmp.findKey(Exiv2::XmpKey("Xmp.xmp.Label")); + if (pos != xmp.end()) { + properties.color.value = defaultColorMapper.index(pos->toString()); + } + } catch (std::exception &exc) { + std::cerr << "ERROR loading thumbnail properties data from " + << rtengine::Exiv2Metadata::xmpSidecarPath(fname) + << ": " << exc.what() << std::endl; + } + } +} + +void Thumbnail::updateProcParamsProperties() +{ + if (!properties.edited()) { + return; + } + + if (properties.trashed.edited && properties.trashed != pparams->inTrash) { + pparams->inTrash = properties.trashed; + pparamsValid = true; + } + + // save procparams rank and color also when options.thumbnailRankColorMode == Options::ThumbnailPropertyMode::XMP + // so they'll be kept in sync + if (properties.rank.edited && properties.rank != pparams->rank) { + pparams->rank = properties.rank; + pparamsValid = true; + } + + if (properties.color.edited && properties.color != pparams->colorlabel) { + pparams->colorlabel = properties.color; + pparamsValid = true; + } +} + +void Thumbnail::saveXMPSidecarProperties() +{ + if (!properties.edited()) { + return; + } + + if (options.thumbnailRankColorMode != Options::ThumbnailPropertyMode::XMP) { + return; + } + + auto fn = rtengine::Exiv2Metadata::xmpSidecarPath(fname); + try { + auto xmp = rtengine::Exiv2Metadata::getXmpSidecar(fname); + if (properties.rank.edited) { + xmp["Xmp.xmp.Rating"] = std::to_string(properties.rank); + } + if (properties.color.edited) { + xmp["Xmp.xmp.Label"] = defaultColorMapper.label(properties.color); + } + + rtengine::Exiv2Metadata meta; + meta.xmpData() = std::move(xmp); + meta.saveToXmp(fn); + } catch (std::exception &exc) { + std::cerr << "ERROR saving thumbnail properties data to " << fn + << ": " << exc.what() << std::endl; + } +} + void Thumbnail::saveMetadata() { if (options.rtSettings.metadata_xmp_sync != rtengine::Settings::MetadataXmpSync::READ_WRITE) { diff --git a/rtgui/thumbnail.h b/rtgui/thumbnail.h index 82c4f4738..c1e52760c 100644 --- a/rtgui/thumbnail.h +++ b/rtgui/thumbnail.h @@ -78,6 +78,30 @@ class Thumbnail bool initial_; + // Properties holds values and edited states for rank, color and trashed + struct Properties { + template struct Property { + T value; + bool edited; + Property(T v): value(v), edited(false) {} + Property& operator=(T v) + { + value = v; + edited = true; + return *this; + } + operator T() const { return value; } + }; + Property rank; + Property color; + Property trashed; + + explicit Properties(int r=0, int c=0, bool t=false): + rank(r), color(c), trashed(t) {} + bool edited() const { return rank.edited || color.edited || trashed.edited; } + }; + Properties properties; + // vector of listeners std::vector listeners; @@ -90,6 +114,9 @@ class Thumbnail Glib::ustring getCacheFileName (const Glib::ustring& subdir, const Glib::ustring& fext) const; void saveMetadata(); + void loadProperties(); + void updateProcParamsProperties(); + void saveXMPSidecarProperties(); public: Thumbnail (CacheManager* cm, const Glib::ustring& fname, CacheImageData* cf); @@ -155,8 +182,8 @@ public: int getColorLabel () const; void setColorLabel (int colorlabel); - int getStage () const; - void setStage (bool stage); + bool getTrashed () const; + void setTrashed (bool trashed); void addThumbnailListener (ThumbnailListener* tnl); void removeThumbnailListener (ThumbnailListener* tnl);