/* This file is from Nitrogen, an X11 background setter. Copyright (C) 2006 Dave Foster & Javeed Shaikh This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //#include "md5.h" #include #include #include #include #include #include #include "thumbview.h" //#include "Config.h" //#include "Util.h" //#include "gcs-i18n.h" /** * Returns the last modified time of a file. * @param file The name of the file to get the modified time for. */ static time_t get_file_mtime(std::string file) { struct stat buf; if (stat(file.c_str(), &buf) == -1) return 0; // error return buf.st_mtime; } /** * Returns the value of the "tEXt::Thumb::MTime" key for fd.o style thumbs. * @param pixbuf The pixbuf of the fd.o thumbnail. */ static time_t get_fdo_thumbnail_mtime(Glib::RefPtr pixbuf) { std::string mtime_str = pixbuf->get_option("tEXt::Thumb::MTime"); std::stringstream stream(mtime_str); time_t mtime = 0; stream >> mtime; return mtime; } void DelayLoadingStore::get_value_vfunc (const iterator& iter, int column, Glib::ValueBase& value) const { g_async_queue_ref(aqueue_loadthumbs); Gtk::ListStore::get_value_vfunc(iter, column, value); Gtk::TreeModel::Row row = *iter; if (column == 0) { Glib::Value< Glib::RefPtr > base; Gtk::ListStore::get_value_vfunc(iter, column, base); Glib::RefPtr thumb = base.get(); if (thumb == thumbview->loading_image && !row[thumbview->record.LoadingThumb]) { TreePair* tp = new TreePair(); tp->file = row[thumbview->record.Filename]; tp->iter = iter; row[thumbview->record.LoadingThumb] = true; //Util::program_log("Custom model: planning on loading %s\n", tp->file.c_str()); g_async_queue_push(aqueue_loadthumbs, (gpointer)tp); } } g_async_queue_unref(aqueue_loadthumbs); } /** * Constructor, sets up gtk stuff, inits data and queues */ Thumbview::Thumbview() : dir("") { set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); set_shadow_type (Gtk::SHADOW_IN); // load map of setbgs (need this for add_file) load_map_setbgs(); // make our async queues this->aqueue_loadthumbs = g_async_queue_new(); this->aqueue_createthumbs = g_async_queue_new(); this->aqueue_donethumbs = g_async_queue_new(); // create store store = DelayLoadingStore::create (record); store->set_queue(aqueue_loadthumbs); store->set_thumbview(this); // setup view view.set_model (store); view.set_headers_visible (FALSE); view.set_fixed_height_mode (TRUE); view.set_rules_hint (TRUE); // set cell renderer proprties rend.property_ellipsize () = Pango::ELLIPSIZE_END; rend.set_property ("ellipsize", Pango::ELLIPSIZE_END); rend.property_weight () = Pango::WEIGHT_BOLD; rend_img.set_fixed_size(105, 82); // make treeviewcolumns this->col_thumb = new Gtk::TreeViewColumn("thumbnail", this->rend_img); this->col_desc = new Gtk::TreeViewColumn("description", this->rend); col_thumb->add_attribute (rend_img, "pixbuf", record.Thumbnail); col_thumb->set_sizing(Gtk::TREE_VIEW_COLUMN_FIXED); col_thumb->set_fixed_width(105); col_desc->add_attribute (rend, "markup", record.Description); col_desc->set_sort_column (record.Filename); col_desc->set_sort_indicator (true); col_desc->set_sort_order (Gtk::SORT_ASCENDING); col_desc->set_sizing(Gtk::TREE_VIEW_COLUMN_FIXED); view.append_column (*col_thumb); view.append_column (*col_desc); // enable search view.set_search_column (record.Description); view.set_search_equal_func (sigc::mem_fun (this, &Thumbview::search_compare)); // load loading image, which not all themes seem to provide try { this->loading_image = Gtk::IconTheme::get_default()->load_icon("image-loading", 64, Gtk::ICON_LOOKUP_FORCE_SVG); } catch (Gtk::IconThemeError e) {} // init dispatcher this->dispatch_thumb.connect(sigc::mem_fun(this, &Thumbview::handle_dispatch_thumb)); col_desc->set_expand (); add (view); view.show (); show (); } /** * Destructor */ Thumbview::~Thumbview() { g_async_queue_unref(this->aqueue_loadthumbs); g_async_queue_unref(this->aqueue_createthumbs); g_async_queue_unref(this->aqueue_donethumbs); } /** * Adds the given file to the tree view and pushes it onto the thumbnail * creation queue. * * @param filename The name of the file to add. * */ void Thumbview::add_file(std::string filename) { Gtk::Window *window = dynamic_cast(get_toplevel()); Gtk::TreeModel::iterator iter = this->store->append (); Gtk::TreeModel::Row row = *iter; Glib::RefPtr thumb = this->loading_image; row[record.Thumbnail] = thumb; row[record.Filename] = filename; row[record.Description] = Glib::ustring(filename, filename.rfind ("/")+1); for (std::map::iterator i = map_setbgs.begin(); i!=map_setbgs.end(); i++) { if (filename == (*i).second) { row[record.CurBGOnDisp] = (*i).first; // row[record.Description] = Util::make_current_set_string(window, filename, (*i).first); } } // for modified time row[record.Time] = get_file_mtime(filename); // Util::program_log("add_file(): Adding file %s\n", filename.c_str()); // push it on the thumb queue // TreePair *tp = new TreePair(); // tp->file = filename; // tp->iter = iter; // queue_thumbs.push(tp); } /** * Opens the internal directory and starts reading files into the async queue. */ void Thumbview::load_dir(std::string dir) { if (!dir.length()) dir = this->dir; std::queue subdirs; Glib::Dir *dirhandle; // push the initial dir back onto subdirs subdirs.push(dir); // loop it while ( ! subdirs.empty() ) { // pop first Glib::ustring curdir = subdirs.front(); subdirs.pop(); try { dirhandle = new Glib::Dir(curdir); // Util::program_log("load_dir(): Opening dir %s\n", curdir.c_str()); } catch (Glib::FileError e) { std::cerr << "Could not open dir" << " " << this->dir << ": " << e.what() << "\n"; continue; } #ifdef USE_INOTIFY // check if we're already monitoring this dir. if (watches.find(curdir) == watches.end()) { // this dir was successfully opened. monitor it for changes with inotify. // the Watch will be cleaned up automatically if the dir is deleted. Inotify::Watch * watch = Inotify::Watch::create(curdir); if (watch) { // no error occurred. // emitted when a file is deleted in this dir. watch->signal_deleted.connect(sigc::mem_fun(this, &Thumbview::file_deleted_callback)); // emitted when a file is modified or created in this dir. watch->signal_write_closed.connect(sigc::mem_fun(this, &Thumbview::file_changed_callback)); // two signals that are emitted when a file is renamed in this dir. // the best way to handle this IMO is to remove the file upon receiving // 'moved_from', and then to add the file upon receiving 'moved_to'. watch->signal_moved_from.connect(sigc::mem_fun(this, &Thumbview::file_deleted_callback)); watch->signal_moved_to.connect(sigc::mem_fun(this, &Thumbview::file_changed_callback)); watch->signal_created.connect(sigc::mem_fun(this, &Thumbview::file_created_callback)); watches[curdir] = watch; } } #endif for (Glib::Dir::iterator i = dirhandle->begin(); i != dirhandle->end(); i++) { Glib::ustring fullstr = curdir + Glib::ustring("/"); try { fullstr += /*Glib::filename_to_utf8(*/*i;//); } catch (Glib::ConvertError& error) { std::cerr << "Invalid UTF-8 encountered. Skipping file" << " " << *i << std::endl; continue; } if ( Glib::file_test(fullstr, Glib::FILE_TEST_IS_DIR) ) { // if ( Config::get_instance()->get_recurse() ) // subdirs.push(fullstr); } else { if ( this->is_image(fullstr) ) { add_file(fullstr); } } } delete dirhandle; } } /** * Tests the file to see if it is an image * TODO: come up with less sux way of doing it than extension * * @param file The filename to test * @return If its an image or not */ bool Thumbview::is_image(std::string file) { if (file.find (".jpg") != std::string::npos || file.find (".JPG") != std::string::npos || file.find (".jpeg") != std::string::npos || file.find (".JPEG") != std::string::npos ) return true; return false; } /** * Determines the full path to a cache file. Does all creation of dirs. * TODO: should throw exception if we cannot create the dirs! * * @param file The file to convert, calls cache_name on it * @return The full path to */ Glib::ustring Thumbview::cache_file(Glib::ustring file) { Glib::ustring urifile = Glib::filename_to_uri(file); /* // compute md5 of file's uri md5_state_t state; md5_byte_t digest[16]; md5_init(&state); md5_append(&state, (const md5_byte_t *)urifile.c_str(), strlen(urifile.c_str())); md5_finish(&state, digest); char *buf = new char[3]; char *full = new char[33]; full[0] = '\0'; for (int di = 0; di < 16; ++di) { sprintf(buf, "%02x", digest[di]); strcat(full, buf); } Glib::ustring md5file = Glib::ustring(full) + Glib::ustring(".png"); delete [] buf; delete [] full; // build dir paths Glib::ustring halfref = Glib::build_filename(Glib::get_home_dir(),".thumbnails/"); halfref = Glib::build_filename(halfref, "normal/"); if ( !Glib::file_test(halfref, Glib::FILE_TEST_EXISTS) ) if ( g_mkdir_with_parents(halfref.c_str(), 0700) == -1) // TODO: exception return "FAIL"; // add and return return Glib::build_filename(halfref, md5file); */ return urifile; } /** * Creates cache images that show up in its queue. */ void Thumbview::load_cache_images() { g_async_queue_ref(this->aqueue_loadthumbs); g_async_queue_ref(this->aqueue_donethumbs); while(1) { // remove first item (blocks until an item occurs) TreePair *p = (TreePair*)g_async_queue_pop(this->aqueue_loadthumbs); Glib::ustring file = p->file; Glib::ustring cachefile = this->cache_file(file); // branch to see if we need to load or create cache file if ( !Glib::file_test(cachefile, Glib::FILE_TEST_EXISTS) ) { g_async_queue_push(this->aqueue_createthumbs,(gpointer)p); } else { // load thumb Glib::RefPtr pb = Gdk::Pixbuf::create_from_file(this->cache_file(file)); // resize if we need to if (pb->get_width() > 100 || pb->get_height() > 80) { int pbwidth = pb->get_width(); int pbheight = pb->get_height(); float ratio = (float)pbwidth / (float)pbheight; int newwidth, newheight; if (abs(100 - pbwidth) > abs(80 - pbheight)) { // cap to vertical newheight = 80; newwidth = newheight * ratio; } else { // cap to horiz newwidth = 100; newheight = newwidth / ratio; } pb = pb->scale_simple(newwidth, newheight, Gdk::INTERP_NEAREST); } if (get_fdo_thumbnail_mtime(pb) < get_file_mtime(file)) { // the thumbnail is old. we need to make a new one. pb.clear(); g_async_queue_push(this->aqueue_createthumbs,(gpointer)p); } else { // display it TreePair *sendp = new TreePair(); sendp->file = file; sendp->iter = p->iter; sendp->thumb = pb; g_async_queue_push(this->aqueue_donethumbs, (gpointer)sendp); this->dispatch_thumb.emit(); delete p; } } } g_async_queue_unref(this->aqueue_loadthumbs); g_async_queue_unref(this->aqueue_donethumbs); throw Glib::Thread::Exit(); } /** * Thread function to create thumbnail cache images for those that do not exist. */ void Thumbview::create_cache_images() { Glib::RefPtr thumb; g_async_queue_ref(this->aqueue_createthumbs); g_async_queue_ref(this->aqueue_donethumbs); // always work! while (1) { // remove first item (blocks when no items occur) TreePair *p = (TreePair*)g_async_queue_pop(this->aqueue_createthumbs); // get filenames Glib::ustring file = p->file; Glib::ustring cachefile = this->cache_file(file); // Util::program_log("create_cache_images(): Caching file %s\n", file.c_str()); // open image try { thumb = Gdk::Pixbuf::create_from_file(file); } catch (...) { // forget it, move on delete p; continue; } // eliminate zero heights (due to really tiny images :/) int height = (int)(100*((float)thumb->get_height()/(float)thumb->get_width())); if (!height) height = 1; // create thumb thumb = thumb->scale_simple(100, height, Gdk::INTERP_TILES); // create required fd.o png tags std::list opts, vals; opts.push_back(Glib::ustring("tEXt::Thumb::URI")); vals.push_back(Glib::filename_to_uri(file)); time_t mtime = get_file_mtime(file); char *bufout = new char[20]; sprintf(bufout, "%d", mtime); opts.push_back(Glib::ustring("tEXt::Thumb::MTime")); vals.push_back(Glib::ustring(bufout)); delete [] bufout; thumb->save(cachefile, "png", opts, vals); // send it to the display TreePair *sendp = new TreePair(); sendp->file = file; sendp->iter = p->iter; sendp->thumb = thumb; g_async_queue_push(this->aqueue_donethumbs, (gpointer)sendp); // emit dispatcher this->dispatch_thumb.emit(); delete p; } g_async_queue_unref(this->aqueue_createthumbs); g_async_queue_unref(this->aqueue_donethumbs); throw Glib::Thread::Exit(); } void Thumbview::handle_dispatch_thumb() { g_async_queue_ref(this->aqueue_donethumbs); TreePair *donep = (TreePair*)g_async_queue_pop(this->aqueue_donethumbs); this->update_thumbnail(donep->file, donep->iter, donep->thumb); delete donep; g_async_queue_unref(this->aqueue_donethumbs); } /** * Updates the treeview to show the passed in thumbnail. * */ void Thumbview::update_thumbnail(Glib::ustring file, Gtk::TreeModel::iterator iter, Glib::RefPtr pb) { Gtk::TreeModel::Row row = *iter; row[record.Thumbnail] = pb; // desc //row[record.Description] = Glib::ustring(file, file.rfind("/")+1); // emit a changed signal store->row_changed(store->get_path(iter), iter); } /** * Used by GTK to see whether or not an iterator matches a search string. */ bool Thumbview::search_compare (const Glib::RefPtr& model, int column, const Glib::ustring& key, const Gtk::TreeModel::iterator& iter) { Glib::ustring target = (*iter)[record.Description]; if (target.find (key) != Glib::ustring::npos) { return false; } return true; } /** * Sets the sort mode. This tells the view how it should sort backgrounds. */ void Thumbview::set_sort_mode (Thumbview::SortMode mode) { switch (mode) { case SORT_ALPHA: store->set_sort_column (record.Description, Gtk::SORT_ASCENDING); break; case SORT_RALPHA: store->set_sort_column (record.Description, Gtk::SORT_DESCENDING); break; case SORT_TIME: store->set_sort_column (record.Time, Gtk::SORT_ASCENDING); break; case SORT_RTIME: store->set_sort_column (record.Time, Gtk::SORT_DESCENDING); break; } } /** * Loads the map of displays and their set bgs. Used to indicate which * files are currently set as a background. */ void Thumbview::load_map_setbgs() { map_setbgs.clear(); std::vector vecgroups; bool ret = false; // bool ret = Config::get_instance()->get_bg_groups(vecgroups); if (!ret) return; for (std::vector::iterator i = vecgroups.begin(); i!=vecgroups.end(); i++) { Glib::ustring file; // SetBG::SetMode mode; Gdk::Color bgcolor; // ret = Config::get_instance()->get_bg(*i, file, mode, bgcolor); if (!ret) { std::cerr << "(load_map_setbgs) Could not get background stored info for " << *i << "\n"; return; } map_setbgs[*i] = file; } } #ifdef USE_INOTIFY /** * Called when a file in a directory being monitored by inotify is deleted. * Removes the file from the tree view. * * @param filename The name of the file to remove. */ void Thumbview::file_deleted_callback(std::string filename) { if (watches.find(filename) != watches.end()) { watches.erase(filename); return; } Gtk::TreeIter iter; Gtk::TreeModel::Children children = store->children(); for (iter = children.begin(); iter != children.end(); iter++) { Glib::ustring this_filename = (*iter)[record.Filename]; if (this_filename == filename) { // remove this iter. store->erase(iter); break; } } } /** * Called when a file in a directory being monitored by inotify is modified * or a new file is created. Adds the file to the tree view. * * @param filename The name of the file modified or created. */ void Thumbview::file_changed_callback(std::string filename) { // first remove the old instance of this file. file_deleted_callback(filename); if ( Glib::file_test(filename, Glib::FILE_TEST_IS_DIR) ) { // this is a directory; use load_dir() to set us up the bomb. load_dir(filename); } else if (is_image(filename)) { // this is a file. add_file(filename); } // restart the idle function //Glib::signal_idle().connect(sigc::mem_fun(this, &Thumbview::load_cache_images)); } /** * Called when a new file or directory is created in a directory being * monitored. Discards the event for non-directories, because * file_changed_callback will be called for those. * * @param filename The name of the file modified or created. */ void Thumbview::file_created_callback(std::string filename) { if ( Glib::file_test(filename, Glib::FILE_TEST_IS_DIR) ) { file_changed_callback(filename); } } #endif