Files
2025-12-10 14:38:26 -08:00

468 lines
15 KiB
C++

// *****************************************************************************
// * 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 <map>
#include <variant>
#include <zen/thread.h> //includes <std/thread.hpp>
#include <wx+/dc.h>
#include <wx+/image_resources.h>
#include <wx+/std_button_layout.h>
#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<ImageHolder, FileIconHolder> 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<AbstractPath>& 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<AbstractPath> 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<wxImage> 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<ImageHolder>(&idata.iconHolder))
{
if (*ih) //if not yet converted...
{
idata.iconImg = std::make_unique<wxImage>(extractWxImage(std::move(*ih))); //convert in main thread!
assert(!*ih);
}
}
else
{
if (FileIconHolder& fih = std::get<FileIconHolder>(idata.iconHolder)) //if not yet converted...
{
idata.iconImg = std::make_unique<wxImage>(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<ImageHolder, FileIconHolder>&& 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<AbstractPath, IconData>;
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<ImageHolder, FileIconHolder> iconHolder; //native icon representation: may be used by any thread
std::unique_ptr<wxImage> 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<Zstring, wxImage, StringHashAsciiNoCase, StringEqualAsciiNoCase> extensionIcons; //no item count limit!? Test case C:\ ~ 3800 unique file extensions
};
IconBuffer::IconBuffer(IconSize sz) : pimpl_(std::make_unique<Impl>()), 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<wxImage> IconBuffer::retrieveFileIcon(const AbstractPath& filePath)
{
const Zstring fileName = AFS::getItemName(filePath);
if (std::optional<wxImage> 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<AbstractPath>& 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";
}