// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "icon_buffer.h" #include #include #include //includes #include #include #include #include "base/icon_loader.h" using namespace zen; using namespace fff; using AFS = AbstractFileSystem; namespace { const size_t BUFFER_SIZE_MAX = 1000; //maximum number of icons to hold in buffer: must be big enough to hold visible icons + preload buffer! } //################################################################################################################################################ std::variant getDisplayIcon(const AbstractPath& itemPath, IconBuffer::IconSize sz) { //1. try to load thumbnails switch (sz) { case IconBuffer::IconSize::small: break; case IconBuffer::IconSize::medium: case IconBuffer::IconSize::large: try { if (ImageHolder ih = AFS::getThumbnailImage(itemPath, IconBuffer::getPixSize(sz))) //throw FileError; optional return value return ih; } catch (FileError&) {} //else: fallback to non-thumbnail icon break; } //2. retrieve file icons try { if (FileIconHolder fih = AFS::getFileIcon(itemPath, IconBuffer::getPixSize(sz))) //throw FileError; optional return value return fih; } catch (FileError&) {} //run getIconByTemplatePath()/genericFileIcon() fallbacks on main thread: //extractWxImage() might fail if icon theme is missing a MIME type! return ImageHolder(); } //################################################################################################################################################ //---------------------- Shared Data ------------------------- class WorkLoad { public: //context of main thread void set(const std::vector& newLoad) { assert(runningOnMainThread()); { std::lock_guard dummy(lockFiles_); workLoad_ = newLoad; } conditionNewWork_.notify_all(); //instead of notify_one(); work around bug: https://svn.boost.org/trac/boost/ticket/7796 //condition handling, see: https://www.boost.org/doc/libs/1_43_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref } void add(const AbstractPath& filePath) //context of main thread { assert(runningOnMainThread()); { std::lock_guard dummy(lockFiles_); workLoad_.emplace_back(filePath); //set as next item to retrieve } conditionNewWork_.notify_all(); } //context of worker thread, blocking: AbstractPath extractNext() //throw ThreadStopRequest { assert(!runningOnMainThread()); std::unique_lock dummy(lockFiles_); interruptibleWait(conditionNewWork_, dummy, [this] { return !workLoad_.empty(); }); //throw ThreadStopRequest AbstractPath filePath = workLoad_. back(); //yes, no strong exception guarantee (std::bad_alloc) /**/ workLoad_.pop_back(); // return filePath; } private: //AbstractPath is thread-safe like an int! std::mutex lockFiles_; std::condition_variable conditionNewWork_; //signal event: data for processing available std::vector workLoad_; //processes last elements of vector first! }; class Buffer { public: //called by main and worker thread: bool hasIcon(const AbstractPath& filePath) const { std::lock_guard dummy(lockIconList_); return iconList.contains(filePath); } //- must be called by main thread only! => wxImage is NOT thread-safe like an int (non-atomic ref-count!!!) //- check wxImage::IsOk() + implement fallback if needed std::optional retrieve(const AbstractPath& filePath) { assert(runningOnMainThread()); std::lock_guard dummy(lockIconList_); auto it = iconList.find(filePath); if (it == iconList.end()) return {}; markAsHot(it); IconData& idata = refData(it); if (ImageHolder* ih = std::get_if(&idata.iconHolder)) { if (*ih) //if not yet converted... { idata.iconImg = std::make_unique(extractWxImage(std::move(*ih))); //convert in main thread! assert(!*ih); } } else { if (FileIconHolder& fih = std::get(idata.iconHolder)) //if not yet converted... { idata.iconImg = std::make_unique(extractWxImage(std::move(fih))); //convert in main thread! assert(!fih); //!idata.iconImg->IsOk(): extractWxImage() might fail if icon theme is missing a MIME type! } } return idata.iconImg ? *idata.iconImg : wxNullImage; //idata.iconHolder may be inserted as empty from worker thread! } //called by main and worker thread: void insert(const AbstractPath& filePath, std::variant&& ih) { std::lock_guard dummy(lockIconList_); //thread safety: moving ImageHolder is free from side effects, but ~wxImage() is NOT! => do NOT delete items from iconList here! const auto [it, inserted] = iconList.try_emplace(filePath); assert(inserted); if (inserted) { refData(it).iconHolder = std::move(ih); priorityListPushBack(it); } } //must be called by main thread only! => ~wxImage() is NOT thread-safe! //call at an appropriate time, e.g. after Workload::set() void limitSize() { assert(runningOnMainThread()); std::lock_guard dummy(lockIconList_); while (iconList.size() > BUFFER_SIZE_MAX) { auto itDelPos = firstInsertPos_; priorityListPopFront(); iconList.erase(itDelPos); //remove oldest element } } private: struct IconData; using FileIconMap = std::map; IconData& refData(FileIconMap::iterator it) { return it->second; } //call while holding lock: void priorityListPopFront() { assert(firstInsertPos_!= iconList.end()); firstInsertPos_ = refData(firstInsertPos_).next; if (firstInsertPos_ != iconList.end()) refData(firstInsertPos_).prev = iconList.end(); else //priority list size > BUFFER_SIZE_MAX in this context, but still for completeness: lastInsertPos_ = iconList.end(); } //call while holding lock: void priorityListPushBack(FileIconMap::iterator it) { if (lastInsertPos_ == iconList.end()) { assert(firstInsertPos_ == iconList.end()); firstInsertPos_ = lastInsertPos_ = it; refData(it).prev = refData(it).next = iconList.end(); } else { refData(it).next = iconList.end(); refData(it).prev = lastInsertPos_; refData(lastInsertPos_).next = it; lastInsertPos_ = it; } } //call while holding lock: void markAsHot(FileIconMap::iterator it) //mark existing buffer entry as if newly inserted { assert(it != iconList.end()); if (refData(it).next != iconList.end()) { if (refData(it).prev != iconList.end()) { refData(refData(it).prev).next = refData(it).next; //remove somewhere from the middle refData(refData(it).next).prev = refData(it).prev; // } else { assert(it == firstInsertPos_); priorityListPopFront(); } priorityListPushBack(it); } else { if (refData(it).prev != iconList.end()) assert(it == lastInsertPos_); //nothing to do else assert(iconList.size() == 1 && it == firstInsertPos_ && it == lastInsertPos_); //nothing to do } } struct IconData { IconData() {} IconData(IconData&& tmp) noexcept : iconHolder(std::move(tmp.iconHolder)), iconImg(std::move(tmp.iconImg)), prev(tmp.prev), next(tmp.next) {} std::variant iconHolder; //native icon representation: may be used by any thread std::unique_ptr iconImg; //use ONLY from main thread! //wxImage is NOT thread-safe: non-atomic ref-count just to begin with... //- prohibit implicit calls to wxImage() //- prohibit calls to ~wxImage() and transitively ~IconData() //- prohibit even wxImage() default constructor - better be safe than sorry! FileIconMap::iterator prev; //store list sorted by time of insertion into buffer FileIconMap::iterator next; // }; mutable std::mutex lockIconList_; FileIconMap iconList; //shared resource; Zstring is thread-safe like an int FileIconMap::iterator firstInsertPos_ = iconList.end(); FileIconMap::iterator lastInsertPos_ = iconList.end(); }; //################################################################################################################################################ //######################### redirect to impl ##################################################### struct IconBuffer::Impl { //communication channel used by threads: WorkLoad workload; //manage life time: enclose InterruptibleThread's (until joined)!!! Buffer buffer; // InterruptibleThread worker; //------------------------- //------------------------- std::unordered_map extensionIcons; //no item count limit!? Test case C:\ ~ 3800 unique file extensions }; IconBuffer::IconBuffer(IconSize sz) : pimpl_(std::make_unique()), iconSizeType_(sz) { pimpl_->worker = InterruptibleThread([&workload = pimpl_->workload, &buffer = pimpl_->buffer, sz] { setCurrentThreadName(Zstr("Icon Buffer")); for (;;) { //start work: blocks until next icon to load is retrieved: const AbstractPath itemPath = workload.extractNext(); //throw ThreadStopRequest if (!buffer.hasIcon(itemPath)) //perf: workload may contain duplicate entries? buffer.insert(itemPath, getDisplayIcon(itemPath, sz)); } }); } IconBuffer::~IconBuffer() { setWorkload({}); //make sure interruption point is always reached! needed??? pimpl_->worker.requestStop(); //end thread life time *before* pimpl_->worker.join(); //IconBuffer::Impl member clean up! } int IconBuffer::getPixSize(IconSize sz) { //coordinate with getIconByIndexImpl() and linkOverlayIcon()! switch (sz) { case IconSize::small: return dipToScreen(getMenuIconDipSize()); case IconSize::medium: return dipToScreen(48); case IconSize::large: return dipToScreen(128); } assert(false); return 0; } bool IconBuffer::readyForRetrieval(const AbstractPath& filePath) { return pimpl_->buffer.hasIcon(filePath); } std::optional IconBuffer::retrieveFileIcon(const AbstractPath& filePath) { const Zstring fileName = AFS::getItemName(filePath); if (std::optional ico = pimpl_->buffer.retrieve(filePath)) { if (ico->IsOk()) return ico; else //fallback return this->getIconByExtension(fileName); //buffered! } //since this icon seems important right now, we don't want to wait until next setWorkload() to start retrieving pimpl_->workload.add(filePath); pimpl_->buffer.limitSize(); return {}; } void IconBuffer::setWorkload(const std::vector& load) { assert(load.size() < BUFFER_SIZE_MAX / 2); pimpl_->workload.set(load); //since buffer can only increase due to new workload, pimpl_->buffer.limitSize(); //this is the place to impose the limit from main thread! } wxImage IconBuffer::getIconByExtension(const Zstring& filePath) { const Zstring& ext = getFileExtension(filePath); assert(runningOnMainThread()); auto it = pimpl_->extensionIcons.find(ext); if (it == pimpl_->extensionIcons.end()) { const Zstring& templateName(ext.empty() ? Zstr("file") : Zstr("file.") + ext); //don't pass actual file name to getIconByTemplatePath(), e.g. "AUTHORS" has own mime type on Linux!!! //=> buffer by extension to minimize buffer-misses! wxImage img; try { img = extractWxImage(getIconByTemplatePath(templateName, getPixSize(iconSizeType_))); //throw SysError } catch (SysError&) {} if (!img.IsOk()) //Linux: not all MIME types have icons! img = IconBuffer::genericFileIcon(iconSizeType_); it = pimpl_->extensionIcons.emplace(ext, img).first; } //need buffer size limit??? return it->second; } wxImage IconBuffer::genericFileIcon(IconSize sz) { try { return extractWxImage(fff::genericFileIcon(IconBuffer::getPixSize(sz))); //throw SysError } catch (SysError&) { assert(false); return wxNullImage; } } wxImage IconBuffer::genericDirIcon(IconSize sz) { try { return extractWxImage(fff::genericDirIcon(IconBuffer::getPixSize(sz))); //throw SysError } catch (SysError&) { assert(false); return wxNullImage; } } wxImage IconBuffer::linkOverlayIcon(IconSize sz) { //coordinate with IconBuffer::getPixSize()! return loadImage([sz] { const int iconSize = IconBuffer::getPixSize(sz); if (iconSize >= dipToScreen(128)) return "file_link_128"; if (iconSize >= dipToScreen( 48)) return "file_link_48"; if (iconSize >= dipToScreen( 20)) return "file_link_20"; return "file_link_16"; }()); } wxImage IconBuffer::plusOverlayIcon(IconSize sz) { //coordinate with IconBuffer::getPixSize()! return loadImage([sz] { const int iconSize = IconBuffer::getPixSize(sz); if (iconSize >= dipToScreen(128)) return "file_plus_128"; if (iconSize >= dipToScreen( 48)) return "file_plus_48"; if (iconSize >= dipToScreen( 20)) return "file_plus_20"; return "file_plus_16"; }()); } wxImage IconBuffer::minusOverlayIcon(IconSize sz) { //coordinate with IconBuffer::getPixSize()! return loadImage([sz] { const int iconSize = IconBuffer::getPixSize(sz); if (iconSize >= dipToScreen(128)) return "file_minus_128"; if (iconSize >= dipToScreen( 48)) return "file_minus_48"; if (iconSize >= dipToScreen( 20)) return "file_minus_20"; return "file_minus_16"; }()); } bool fff::hasLinkExtension(const Zstring& filepath) { const Zstring& ext = getFileExtension(filepath); return ext == "desktop"; }