diff options
Diffstat (limited to 'lib/icon_buffer.cpp')
-rw-r--r-- | lib/icon_buffer.cpp | 600 |
1 files changed, 600 insertions, 0 deletions
diff --git a/lib/icon_buffer.cpp b/lib/icon_buffer.cpp new file mode 100644 index 00000000..bb75a538 --- /dev/null +++ b/lib/icon_buffer.cpp @@ -0,0 +1,600 @@ +// ************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl.html * +// * Copyright (C) 2008-2011 ZenJu (zhnmju123 AT gmx.de) * +// ************************************************************************** + +#include "icon_buffer.h" +#include <queue> +#include <set> +#include <zen/thread.h> //includes <boost/thread.hpp> +#include <zen/scope_guard.h> +#include <boost/thread/once.hpp> + +#ifdef FFS_WIN +#include <zen/win.h> //includes "windows.h" +#include <zen/dll.h> +#include "Thumbnail/thumbnail.h" +#include <zen/win_ver.h> + +#elif defined FFS_LINUX +#include <giomm/file.h> +#include <gtkmm/icontheme.h> +#include <gtkmm/main.h> +#endif + +using namespace zen; + + +const size_t BUFFER_SIZE_MAX = 800; //maximum number of icons to buffer + + +int zen::IconBuffer::cvrtSize(IconSize sz) //get size in pixel +{ + switch (sz) + { + case SIZE_SMALL: +#ifdef FFS_WIN + return 16; +#elif defined FFS_LINUX + return 24; +#endif + case SIZE_MEDIUM: + return 48; + case SIZE_LARGE: + return 128; + } + assert(false); + return 0; +} + + +class IconHolder //handle HICON/GdkPixbuf ownership WITHOUT ref-counting to allow thread-safe usage (in contrast to wxIcon) +{ +public: +#ifdef FFS_WIN + typedef HICON HandleType; +#elif defined FFS_LINUX + typedef GdkPixbuf* HandleType; +#endif + + IconHolder(HandleType handle = 0) : handle_(handle) {} //take ownership! + + //icon holder has value semantics! + IconHolder(const IconHolder& other) : handle_(other.handle_ == NULL ? NULL : +#ifdef FFS_WIN + ::CopyIcon(other.handle_) +#elif defined FFS_LINUX + ::gdk_pixbuf_copy(other.handle_) //create new Pix buf with reference count 1 or return 0 on error +#endif + ) {} + + IconHolder& operator=(const IconHolder& other) + { + IconHolder(other).swap(*this); + return *this; + } + + ~IconHolder() + { + if (handle_ != NULL) +#ifdef FFS_WIN + ::DestroyIcon(handle_); +#elif defined FFS_LINUX + ::g_object_unref(handle_); +#endif + } + + void swap(IconHolder& other) { std::swap(handle_, other.handle_); } //throw() + + wxIcon toWxIcon(int expectedSize) const //copy HandleType, caller needs to take ownership! + { + if (handle_ == NULL) + return wxNullIcon; + + IconHolder clone(*this); + + wxIcon newIcon; //attention: wxIcon uses reference counting! +#ifdef FFS_WIN + newIcon.SetHICON(clone.handle_); + + { + //this block costs ~0.04 ms + ICONINFO icoInfo = {}; + if (::GetIconInfo(clone.handle_, &icoInfo)) + { + ::DeleteObject(icoInfo.hbmMask); //nice potential for a GDI leak! + ZEN_ON_BLOCK_EXIT(::DeleteObject(icoInfo.hbmColor)); // + + BITMAP bmpInfo = {}; + if (::GetObject(icoInfo.hbmColor, //__in HGDIOBJ hgdiobj, + sizeof(BITMAP), //__in int cbBuffer, + &bmpInfo) != 0) // __out LPVOID lpvObject + { + const int maxExtent = std::max(bmpInfo.bmWidth, bmpInfo.bmHeight); + if (maxExtent > expectedSize) + { + bmpInfo.bmWidth = bmpInfo.bmWidth * expectedSize / maxExtent; //scale those Vista jumbo 256x256 icons down! + bmpInfo.bmHeight = bmpInfo.bmHeight * expectedSize / maxExtent; // + } + newIcon.SetSize(bmpInfo.bmWidth, bmpInfo.bmHeight); //wxIcon is stretched to this size + } + } + } + //no stretching for now + //newIcon.SetSize(defaultSize, defaultSize); //icon is stretched to this size if referenced HICON differs + +#elif defined FFS_LINUX // + newIcon.SetPixbuf(clone.handle_); // transfer ownership!! +#endif // + clone.handle_ = NULL; // + return newIcon; + } + +private: + HandleType handle_; + struct ConversionToBool { int dummy; }; + +public: + //use member pointer as implicit conversion to bool (C++ Templates - Vandevoorde/Josuttis; chapter 20) + operator int ConversionToBool::* () const { return handle_ != NULL ? &ConversionToBool::dummy : NULL; } +}; + + +#ifdef FFS_WIN +namespace +{ +Zstring getFileExtension(const Zstring& filename) +{ + const Zstring shortName = afterLast(filename, Zchar('\\')); //warning: using windows file name separator! + + return shortName.find(Zchar('.')) != Zstring::npos ? + afterLast(filename, Zchar('.')) : + Zstring(); +} + +std::set<Zstring, LessFilename> priceyExtensions; //thread-safe! +boost::once_flag initExtensionsOnce = BOOST_ONCE_INIT; // + + +//test for extension for non-thumbnail icons that physically have to be retrieved from disc +bool isCheapExtension(const Zstring& extension) +{ + boost::call_once(initExtensionsOnce, []() + { + priceyExtensions.insert(L"exe"); + priceyExtensions.insert(L"lnk"); + priceyExtensions.insert(L"ico"); + priceyExtensions.insert(L"ani"); + priceyExtensions.insert(L"cur"); + priceyExtensions.insert(L"url"); + priceyExtensions.insert(L"msc"); + priceyExtensions.insert(L"scr"); + }); + return priceyExtensions.find(extension) == priceyExtensions.end(); +} + + +bool wereVistaOrLater = false; +boost::once_flag initVistaFlagOnce = BOOST_ONCE_INIT; + + +int getShilIconType(IconBuffer::IconSize sz) +{ + boost::call_once(initVistaFlagOnce, []() + { + wereVistaOrLater = vistaOrLater(); + }); + + switch (sz) + { + case IconBuffer::SIZE_SMALL: + return SHIL_SMALL; //16x16, but the size can be customized by the user. + case IconBuffer::SIZE_MEDIUM: + return SHIL_EXTRALARGE; //typically 48x48, but the size can be customized by the user. + case IconBuffer::SIZE_LARGE: + return wereVistaOrLater ? SHIL_JUMBO //normally 256x256 pixels -> will be scaled down by IconHolder + : SHIL_EXTRALARGE; //XP doesn't have jumbo icons + } + return SHIL_SMALL; +} + + +DllFun<thumb::GetIconByIndexFct> getIconByIndex; +boost::once_flag initGetIconByIndexOnce = BOOST_ONCE_INIT; + + +IconHolder getIconByAttribute(LPCWSTR pszPath, DWORD dwFileAttributes, IconBuffer::IconSize sz) +{ + //NOTE: CoInitializeEx()/CoUninitialize() needs to be called for THIS thread! + SHFILEINFO fileInfo = {}; //initialize hIcon + DWORD_PTR imgList = ::SHGetFileInfo(pszPath, //Windows 7 doesn't like this parameter to be an empty string + dwFileAttributes, + &fileInfo, + sizeof(fileInfo), + SHGFI_SYSICONINDEX | SHGFI_USEFILEATTRIBUTES); + //no need to IUnknown::Release() imgList! + if (!imgList) + return NULL; + + boost::call_once(initGetIconByIndexOnce, []() //thread-safe init + { + getIconByIndex = DllFun<thumb::GetIconByIndexFct>(thumb::getDllName(), thumb::getIconByIndexFctName); + }); + return getIconByIndex ? static_cast<HICON>(getIconByIndex(fileInfo.iIcon, getShilIconType(sz))) : NULL; +} + + +IconHolder getAssociatedIconByExt(const Zstring& extension, IconBuffer::IconSize sz) +{ + //no read-access to disk! determine icon by extension + return getIconByAttribute((Zstr("dummy.") + extension).c_str(), FILE_ATTRIBUTE_NORMAL, sz); +} + + +DllFun<thumb::GetThumbnailFct> getThumbnailIcon; +boost::once_flag initThumbnailOnce = BOOST_ONCE_INIT; +} +#endif +//################################################################################################################################################ + + +IconHolder getThumbnail(const Zstring& filename, int requestedSize) //return 0 on failure +{ +#ifdef FFS_WIN + using namespace thumb; + + boost::call_once(initThumbnailOnce, []() //note: "getThumbnail" function itself is already thread-safe + { + getThumbnailIcon = DllFun<GetThumbnailFct>(getDllName(), getThumbnailFctName); + }); + return getThumbnailIcon ? static_cast< ::HICON>(getThumbnailIcon(filename.c_str(), requestedSize)) : NULL; + +#elif defined FFS_LINUX + //call Gtk::Main::init_gtkmm_internals() on application startup!! + try + { + Glib::RefPtr<Gdk::Pixbuf> iconPixbuf = Gdk::Pixbuf::create_from_file(filename.c_str(), requestedSize, requestedSize); + if (iconPixbuf) + return IconHolder(iconPixbuf->gobj_copy()); //copy and pass icon ownership (may be 0) + } + catch (const Glib::Error&) {} + + return IconHolder(); +#endif +} + + +IconHolder getAssociatedIcon(const Zstring& filename, IconBuffer::IconSize sz) +{ + //1. try to load thumbnails + switch (sz) + { + case IconBuffer::SIZE_SMALL: + break; + case IconBuffer::SIZE_MEDIUM: + case IconBuffer::SIZE_LARGE: + { + IconHolder ico = getThumbnail(filename, IconBuffer::cvrtSize(sz)); + if (ico) + return ico; + //else: fallback to non-thumbnail icon + } + break; + } + + //2. retrieve file icons +#ifdef FFS_WIN + //perf: optimize fallback case for SIZE_MEDIUM and SIZE_LARGE: + const Zstring& extension = getFileExtension(filename); + if (isCheapExtension(extension)) //"pricey" extensions are stored with fullnames and are read from disk, while cheap ones require just the extension + return getAssociatedIconByExt(extension, sz); + //result will not be buffered under extension name, but full filename; this is okay, since we're in SIZE_MEDIUM or SIZE_LARGE context, + //which means the access to get thumbnail failed: thumbnail failure is not dependent from extension in general! + + SHFILEINFO fileInfo = {}; + DWORD_PTR imgList = ::SHGetFileInfo(filename.c_str(), //zen::removeLongPathPrefix(fileName), //::SHGetFileInfo() can't handle \\?\-prefix! + 0, + &fileInfo, + sizeof(fileInfo), + SHGFI_SYSICONINDEX); + //Quote: "The IImageList pointer type, such as that returned in the ppv parameter, can be cast as an HIMAGELIST as + // needed; for example, for use in a list view. Conversely, an HIMAGELIST can be cast as a pointer to an IImageList." + //http://msdn.microsoft.com/en-us/library/windows/desktop/bb762185(v=vs.85).aspx + + if (!imgList) + return NULL; + //imgList->Release(); //empiric study: crash on XP if we release this! Seems we do not own it... -> also no GDI leak on Win7 -> okay + //another comment on http://msdn.microsoft.com/en-us/library/bb762179(v=VS.85).aspx describes exact same behavior on Win7/XP + + boost::call_once(initGetIconByIndexOnce, []() //thread-safe init + { + getIconByIndex = DllFun<thumb::GetIconByIndexFct>(thumb::getDllName(), thumb::getIconByIndexFctName); + }); + return getIconByIndex ? static_cast<HICON>(getIconByIndex(fileInfo.iIcon, getShilIconType(sz))) : NULL; + +#elif defined FFS_LINUX + const int requestedSize = IconBuffer::cvrtSize(sz); + //call Gtk::Main::init_gtkmm_internals() on application startup!! + try + { + Glib::RefPtr<Gio::File> fileObj = Gio::File::create_for_path(filename.c_str()); //never fails + Glib::RefPtr<Gio::FileInfo> fileInfo = fileObj->query_info(G_FILE_ATTRIBUTE_STANDARD_ICON); + if (fileInfo) + { + Glib::RefPtr<Gio::Icon> gicon = fileInfo->get_icon(); + if (gicon) + { + Glib::RefPtr<Gtk::IconTheme> iconTheme = Gtk::IconTheme::get_default(); + if (iconTheme) + { + Gtk::IconInfo iconInfo = iconTheme->lookup_icon(gicon, requestedSize, Gtk::ICON_LOOKUP_USE_BUILTIN); //this may fail if icon is not installed on system + if (iconInfo) + { + Glib::RefPtr<Gdk::Pixbuf> iconPixbuf = iconInfo.load_icon(); //render icon into Pixbuf + if (iconPixbuf) + return IconHolder(iconPixbuf->gobj_copy()); //copy and pass icon ownership (may be 0) + } + } + } + } + } + catch (const Glib::Error&) {} + + try //fallback: icon lookup may fail because some icons are currently not present on system + { + Glib::RefPtr<Gtk::IconTheme> iconTheme = Gtk::IconTheme::get_default(); + if (iconTheme) + { + Glib::RefPtr<Gdk::Pixbuf> iconPixbuf = iconTheme->load_icon("misc", requestedSize, Gtk::ICON_LOOKUP_USE_BUILTIN); + if (!iconPixbuf) + iconPixbuf = iconTheme->load_icon("text-x-generic", requestedSize, Gtk::ICON_LOOKUP_USE_BUILTIN); + if (iconPixbuf) + return IconHolder(iconPixbuf->gobj_copy()); //copy and pass icon ownership (may be 0) + } + } + catch (const Glib::Error&) {} + + //fallback fallback + return IconHolder(); +#endif +} + + +IconHolder getDirectoryIcon(IconBuffer::IconSize sz) +{ +#ifdef FFS_WIN + return getIconByAttribute(L"dummy", //Windows 7 doesn't like this parameter to be an empty string! + FILE_ATTRIBUTE_DIRECTORY, sz); +#elif defined FFS_LINUX + return ::getAssociatedIcon(Zstr("/usr/"), sz); //all directories will look like "/usr/" +#endif +} + + +IconHolder getFileIcon(IconBuffer::IconSize sz) +{ +#ifdef FFS_WIN + return getIconByAttribute(L"dummy", FILE_ATTRIBUTE_NORMAL, sz); +#elif defined FFS_LINUX + const int requestedSize = IconBuffer::cvrtSize(sz); + try + { + Glib::RefPtr<Gtk::IconTheme> iconTheme = Gtk::IconTheme::get_default(); + if (iconTheme) + { + Glib::RefPtr<Gdk::Pixbuf> iconPixbuf = iconTheme->load_icon("misc", requestedSize, Gtk::ICON_LOOKUP_USE_BUILTIN); + if (!iconPixbuf) + iconPixbuf = iconTheme->load_icon("text-x-generic", requestedSize, Gtk::ICON_LOOKUP_USE_BUILTIN); + if (iconPixbuf) + return IconHolder(iconPixbuf->gobj_copy()); // transfer ownership!! + } + } + catch (const Glib::Error&) {} + + return IconHolder(); +#endif +} +//################################################################################################################################################ + + +//---------------------- Shared Data ------------------------- +struct WorkLoad +{ +public: + Zstring extractNextFile() //context of worker thread, blocking + { + boost::unique_lock<boost::mutex> dummy(lockFiles); + + while (filesToLoad.empty()) + conditionNewFiles.timed_wait(dummy, boost::get_system_time() + boost::posix_time::milliseconds(50)); //interruption point! + + Zstring fileName = filesToLoad.back(); + filesToLoad.pop_back(); + return fileName; + } + + void setWorkload(const std::vector<Zstring>& newLoad) //context of main thread + { + { + boost::unique_lock<boost::mutex> dummy(lockFiles); + filesToLoad = newLoad; + } + conditionNewFiles.notify_one(); + //condition handling, see: http://www.boost.org/doc/libs/1_43_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref + } + +private: + std::vector<Zstring> filesToLoad; //processes last elements of vector first! + boost::mutex lockFiles; + boost::condition_variable conditionNewFiles; //signal event: data for processing available +}; + + +typedef std::map<Zstring, IconHolder, LessFilename> NameIconMap; //entryName/icon -> note: Zstring is thread-safe +typedef std::queue<Zstring> IconDbSequence; //entryName + +class Buffer +{ +public: + bool requestFileIcon(const Zstring& fileName, IconHolder* icon = NULL) + { + boost::lock_guard<boost::mutex> dummy(lockBuffer); + + auto iter = iconMappping.find(fileName); + if (iter != iconMappping.end()) + { + if (icon != NULL) + *icon = iter->second; + return true; + } + return false; + } + + void insertIntoBuffer(const Zstring& entryName, const IconHolder& icon) //called by worker thread + { + boost::lock_guard<boost::mutex> dummy(lockBuffer); + + //thread saftey: icon uses ref-counting! But is NOT shared with main thread! + auto rc = iconMappping.insert(std::make_pair(entryName, icon)); + if (rc.second) //if insertion took place + iconSequence.push(entryName); //note: sharing Zstring with IconDB!!! + + assert(iconMappping.size() == iconSequence.size()); + + //remove elements if buffer becomes too big: + if (iconMappping.size() > BUFFER_SIZE_MAX) //limit buffer size: critical because GDI resources are limited (e.g. 10000 on XP per process) + { + //remove oldest element + iconMappping.erase(iconSequence.front()); + iconSequence.pop(); + } + } + +private: + boost::mutex lockBuffer; + NameIconMap iconMappping; //use synchronisation when accessing this! + IconDbSequence iconSequence; //save sequence of buffer entry to delete oldest elements +}; +//------------------------------------------------------------- +//################################################################################################################################################ + +class WorkerThread //lifetime is part of icon buffer +{ +public: + WorkerThread(const std::shared_ptr<WorkLoad>& workload, + const std::shared_ptr<Buffer>& buffer, + IconBuffer::IconSize sz) : + workload_(workload), + buffer_(buffer), + icoSize(sz) {} + + void operator()(); //thread entry + +private: + std::shared_ptr<WorkLoad> workload_; //main/worker thread may access different shared_ptr instances safely (even though they have the same target!) + std::shared_ptr<Buffer> buffer_; //http://www.boost.org/doc/libs/1_43_0/libs/smart_ptr/shared_ptr.htm?sess=8153b05b34d890e02d48730db1ff7ddc#ThreadSafety + const IconBuffer::IconSize icoSize; +}; + + +void WorkerThread::operator()() //thread entry +{ + //failure to initialize COM for each thread is a source of hard to reproduce bugs: https://sourceforge.net/tracker/?func=detail&aid=3160472&group_id=234430&atid=1093080 +#ifdef FFS_WIN + //Prerequisites, see thumbnail.h + + //1. Initialize COM + ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + ZEN_ON_BLOCK_EXIT(::CoUninitialize()); + + //2. Initialize system image list + typedef BOOL (WINAPI *FileIconInitFun)(BOOL fRestoreCache); + const SysDllFun<FileIconInitFun> fileIconInit(L"Shell32.dll", reinterpret_cast<LPCSTR>(660)); + assert(fileIconInit); + if (fileIconInit) + fileIconInit(false); //TRUE to restore the system image cache from disk; FALSE otherwise. +#endif + + while (true) + { + boost::this_thread::interruption_point(); + + const Zstring fileName = workload_->extractNextFile(); //start work: get next icon to load + + if (buffer_->requestFileIcon(fileName)) + continue; //icon already in buffer: skip + + buffer_->insertIntoBuffer(fileName, getAssociatedIcon(fileName, icoSize)); + } +} +//######################### redirect to impl ##################################################### + +struct IconBuffer::Pimpl +{ + Pimpl() : + workload(std::make_shared<WorkLoad>()), + buffer(std::make_shared<Buffer>()) {} + + std::shared_ptr<WorkLoad> workload; + std::shared_ptr<Buffer> buffer; + + boost::thread worker; +}; + + +IconBuffer::IconBuffer(IconSize sz) : + pimpl(new Pimpl), + icoSize(sz), + genDirIcon(::getDirectoryIcon(sz).toWxIcon(cvrtSize(icoSize))), + genFileIcon(::getFileIcon(sz).toWxIcon(cvrtSize(icoSize))) +{ + pimpl->worker = boost::thread(WorkerThread(pimpl->workload, pimpl->buffer, sz)); +} + + +IconBuffer::~IconBuffer() +{ + setWorkload(std::vector<Zstring>()); //make sure interruption point is always reached! + pimpl->worker.interrupt(); + pimpl->worker.join(); +} + + +bool IconBuffer::requestFileIcon(const Zstring& filename, wxIcon* icon) +{ + auto getIcon = [&](const Zstring& entryName) -> bool + { + if (!icon) + return pimpl->buffer->requestFileIcon(entryName); + + IconHolder heldIcon; + if (!pimpl->buffer->requestFileIcon(entryName, &heldIcon)) + return false; + *icon = heldIcon.toWxIcon(cvrtSize(icoSize)); + return true; + }; + +#ifdef FFS_WIN + //perf: let's read icons which don't need file access right away! No async delay justified! + if (icoSize == IconBuffer::SIZE_SMALL) //non-thumbnail view, we need file type icons only! + { + const Zstring& extension = getFileExtension(filename); + if (isCheapExtension(extension)) //"pricey" extensions are stored with fullnames and are read from disk, while cheap ones require just the extension + { + if (!getIcon(extension)) + { + IconHolder heldIcon = getAssociatedIconByExt(extension, icoSize); //fast! + pimpl->buffer->insertIntoBuffer(extension, heldIcon); + if (icon) + *icon = heldIcon.toWxIcon(cvrtSize(icoSize)); + } + return true; + } + } +#endif + + return getIcon(filename); +} + +void IconBuffer::setWorkload(const std::vector<Zstring>& load) { pimpl->workload->setWorkload(load); } |