// ************************************************************************** // * 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 #include #include "../shared/boost_thread_wrap.h" //include #include "../shared/loki/ScopeGuard.h" #include #ifdef FFS_WIN #include //includes "windows.h" #include "../shared/dll_loader.h" #include "../shared/Thumbnail/thumbnail.h" #elif defined FFS_LINUX #include #include #include #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! LOKI_ON_BLOCK_EXIT2(::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 ? filename.AfterLast(Zchar('.')) : Zstring(); } std::set 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 vistaOrLater() { OSVERSIONINFO osvi = {}; osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); //IFileOperation is supported with Vista and later if (::GetVersionEx(&osvi)) return osvi.dwMajorVersion > 5; //XP has majorVersion == 5, minorVersion == 1 //Vista has majorVersion == 6, minorVersion == 0 //version overview: http://msdn.microsoft.com/en-us/library/ms724834(VS.85).aspx return false; } 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; } util::DllFun 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 = util::DllFun(thumb::getDllName(), thumb::getIconByIndexFctName); }); return getIconByIndex ? static_cast(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); } util::DllFun 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 = util::DllFun(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 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 = util::DllFun(thumb::getDllName(), thumb::getIconByIndexFctName); }); return getIconByIndex ? static_cast(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 fileObj = Gio::File::create_for_path(filename.c_str()); //never fails Glib::RefPtr fileInfo = fileObj->query_info(G_FILE_ATTRIBUTE_STANDARD_ICON); if (fileInfo) { Glib::RefPtr gicon = fileInfo->get_icon(); if (gicon) { Glib::RefPtr 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 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 iconTheme = Gtk::IconTheme::get_default(); if (iconTheme) { Glib::RefPtr 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 iconTheme = Gtk::IconTheme::get_default(); if (iconTheme) { Glib::RefPtr 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 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& newLoad) //context of main thread { { boost::unique_lock 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 filesToLoad; //processes last elements of vector first! boost::mutex lockFiles; boost::condition_variable conditionNewFiles; //signal event: data for processing available }; typedef std::map NameIconMap; //entryName/icon -> note: Zstring is thread-safe typedef std::queue IconDbSequence; //entryName class Buffer { public: bool requestFileIcon(const Zstring& fileName, IconHolder* icon = NULL) { boost::lock_guard 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 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, const std::shared_ptr& buffer, IconBuffer::IconSize sz) : workload_(workload), buffer_(buffer), icoSize(sz) {} void operator()(); //thread entry private: std::shared_ptr workload_; //main/worker thread may access different shared_ptr instances safely (even though they have the same target!) std::shared_ptr 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); LOKI_ON_BLOCK_EXIT2(::CoUninitialize()); //2. Initialize system image list typedef BOOL (WINAPI *FileIconInitFun)(BOOL fRestoreCache); const util::DllFun fileIconInit(L"Shell32.dll", reinterpret_cast(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()), buffer(std::make_shared()) {} std::shared_ptr workload; std::shared_ptr 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()); //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& load) { pimpl->workload->setWorkload(load); }