mime_util_xdg.cc revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2010 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "base/mime_util.h" 6 7#include <gtk/gtk.h> 8#include <sys/time.h> 9#include <time.h> 10 11#include <cstdlib> 12#include <list> 13#include <map> 14#include <vector> 15 16#include "base/file_util.h" 17#include "base/logging.h" 18#include "base/message_loop.h" 19#include "base/scoped_ptr.h" 20#include "base/singleton.h" 21#include "base/string_split.h" 22#include "base/string_util.h" 23#include "base/third_party/xdg_mime/xdgmime.h" 24#include "base/threading/thread_restrictions.h" 25 26namespace { 27 28class IconTheme; 29 30class MimeUtilConstants { 31 public: 32 static MimeUtilConstants* GetInstance() { 33 return Singleton<MimeUtilConstants>::get(); 34 } 35 36 // In seconds, specified by icon theme specs. 37 const int kUpdateInterval; 38 39 // Store icon directories and their mtimes. 40 std::map<FilePath, int>* icon_dirs_; 41 42 // Store icon formats. 43 std::vector<std::string> icon_formats_; 44 45 // Store loaded icon_theme. 46 std::map<std::string, IconTheme*>* icon_themes_; 47 48 static const size_t kDefaultThemeNum = 4; 49 50 // The default theme. 51 IconTheme* default_themes_[kDefaultThemeNum]; 52 53 time_t last_check_time_; 54 55 // This is set by DetectGtkTheme(). We cache it so that we can access the 56 // theme name from threads that aren't allowed to call 57 // gtk_settings_get_default(). 58 std::string gtk_theme_name_; 59 60 private: 61 MimeUtilConstants() 62 : kUpdateInterval(5), 63 icon_dirs_(NULL), 64 icon_themes_(NULL), 65 last_check_time_(0) { 66 icon_formats_.push_back(".png"); 67 icon_formats_.push_back(".svg"); 68 icon_formats_.push_back(".xpm"); 69 70 for (size_t i = 0; i < kDefaultThemeNum; ++i) 71 default_themes_[i] = NULL; 72 } 73 ~MimeUtilConstants(); 74 75 friend struct DefaultSingletonTraits<MimeUtilConstants>; 76 77 DISALLOW_COPY_AND_ASSIGN(MimeUtilConstants); 78}; 79 80// IconTheme represents an icon theme as defined by the xdg icon theme spec. 81// Example themes on GNOME include 'Human' and 'Mist'. 82// Example themes on KDE include 'crystalsvg' and 'kdeclassic'. 83class IconTheme { 84 public: 85 // A theme consists of multiple sub-directories, like '32x32' and 'scalable'. 86 class SubDirInfo { 87 public: 88 // See spec for details. 89 enum Type { 90 Fixed, 91 Scalable, 92 Threshold 93 }; 94 SubDirInfo() 95 : size(0), 96 type(Threshold), 97 max_size(0), 98 min_size(0), 99 threshold(2) { 100 } 101 size_t size; // Nominal size of the icons in this directory. 102 Type type; // Type of the icon size. 103 size_t max_size; // Maximum size that the icons can be scaled to. 104 size_t min_size; // Minimum size that the icons can be scaled to. 105 size_t threshold; // Maximum difference from desired size. 2 by default. 106 }; 107 108 explicit IconTheme(const std::string& name); 109 110 ~IconTheme() { 111 delete[] info_array_; 112 } 113 114 // Returns the path to an icon with the name |icon_name| and a size of |size| 115 // pixels. If the icon does not exist, but |inherits| is true, then look for 116 // the icon in the parent theme. 117 FilePath GetIconPath(const std::string& icon_name, int size, bool inherits); 118 119 // Load a theme with the name |theme_name| into memory. Returns null if theme 120 // is invalid. 121 static IconTheme* LoadTheme(const std::string& theme_name); 122 123 private: 124 // Returns the path to an icon with the name |icon_name| in |subdir|. 125 FilePath GetIconPathUnderSubdir(const std::string& icon_name, 126 const std::string& subdir); 127 128 // Whether the theme loaded properly. 129 bool IsValid() { 130 return index_theme_loaded_; 131 } 132 133 // Read and parse |file| which is usually named 'index.theme' per theme spec. 134 bool LoadIndexTheme(const FilePath& file); 135 136 // Checks to see if the icons in |info| matches |size| (in pixels). Returns 137 // 0 if they match, or the size difference in pixels. 138 size_t MatchesSize(SubDirInfo* info, size_t size); 139 140 // Yet another function to read a line. 141 std::string ReadLine(FILE* fp); 142 143 // Set directories to search for icons to the comma-separated list |dirs|. 144 bool SetDirectories(const std::string& dirs); 145 146 bool index_theme_loaded_; // True if an instance is properly loaded. 147 // store the scattered directories of this theme. 148 std::list<FilePath> dirs_; 149 150 // store the subdirs of this theme and array index of |info_array_|. 151 std::map<std::string, int> subdirs_; 152 SubDirInfo* info_array_; // List of sub-directories. 153 std::string inherits_; // Name of the theme this one inherits from. 154}; 155 156IconTheme::IconTheme(const std::string& name) 157 : index_theme_loaded_(false), 158 info_array_(NULL) { 159 base::ThreadRestrictions::AssertIOAllowed(); 160 // Iterate on all icon directories to find directories of the specified 161 // theme and load the first encountered index.theme. 162 std::map<FilePath, int>::iterator iter; 163 FilePath theme_path; 164 std::map<FilePath, int>* icon_dirs = 165 MimeUtilConstants::GetInstance()->icon_dirs_; 166 for (iter = icon_dirs->begin(); iter != icon_dirs->end(); ++iter) { 167 theme_path = iter->first.Append(name); 168 if (!file_util::DirectoryExists(theme_path)) 169 continue; 170 FilePath theme_index = theme_path.Append("index.theme"); 171 if (!index_theme_loaded_ && file_util::PathExists(theme_index)) { 172 if (!LoadIndexTheme(theme_index)) 173 return; 174 index_theme_loaded_ = true; 175 } 176 dirs_.push_back(theme_path); 177 } 178} 179 180FilePath IconTheme::GetIconPath(const std::string& icon_name, int size, 181 bool inherits) { 182 std::map<std::string, int>::iterator subdir_iter; 183 FilePath icon_path; 184 185 for (subdir_iter = subdirs_.begin(); 186 subdir_iter != subdirs_.end(); 187 ++subdir_iter) { 188 SubDirInfo* info = &info_array_[subdir_iter->second]; 189 if (MatchesSize(info, size) == 0) { 190 icon_path = GetIconPathUnderSubdir(icon_name, subdir_iter->first); 191 if (!icon_path.empty()) 192 return icon_path; 193 } 194 } 195 // Now looking for the mostly matched. 196 int min_delta_seen = 9999; 197 198 for (subdir_iter = subdirs_.begin(); 199 subdir_iter != subdirs_.end(); 200 ++subdir_iter) { 201 SubDirInfo* info = &info_array_[subdir_iter->second]; 202 int delta = abs(MatchesSize(info, size)); 203 if (delta < min_delta_seen) { 204 FilePath path = GetIconPathUnderSubdir(icon_name, subdir_iter->first); 205 if (!path.empty()) { 206 min_delta_seen = delta; 207 icon_path = path; 208 } 209 } 210 } 211 212 if (!icon_path.empty() || !inherits || inherits_ == "") 213 return icon_path; 214 215 IconTheme* theme = LoadTheme(inherits_); 216 // Inheriting from itself means the theme is buggy but we shouldn't crash. 217 if (theme && theme != this) 218 return theme->GetIconPath(icon_name, size, inherits); 219 else 220 return FilePath(); 221} 222 223IconTheme* IconTheme::LoadTheme(const std::string& theme_name) { 224 scoped_ptr<IconTheme> theme; 225 std::map<std::string, IconTheme*>* icon_themes = 226 MimeUtilConstants::GetInstance()->icon_themes_; 227 if (icon_themes->find(theme_name) != icon_themes->end()) { 228 theme.reset((*icon_themes)[theme_name]); 229 } else { 230 theme.reset(new IconTheme(theme_name)); 231 if (!theme->IsValid()) 232 theme.reset(); 233 (*icon_themes)[theme_name] = theme.get(); 234 } 235 return theme.release(); 236} 237 238FilePath IconTheme::GetIconPathUnderSubdir(const std::string& icon_name, 239 const std::string& subdir) { 240 FilePath icon_path; 241 std::list<FilePath>::iterator dir_iter; 242 std::vector<std::string>* icon_formats = 243 &MimeUtilConstants::GetInstance()->icon_formats_; 244 for (dir_iter = dirs_.begin(); dir_iter != dirs_.end(); ++dir_iter) { 245 for (size_t i = 0; i < icon_formats->size(); ++i) { 246 icon_path = dir_iter->Append(subdir); 247 icon_path = icon_path.Append(icon_name + (*icon_formats)[i]); 248 if (file_util::PathExists(icon_path)) 249 return icon_path; 250 } 251 } 252 return FilePath(); 253} 254 255bool IconTheme::LoadIndexTheme(const FilePath& file) { 256 FILE* fp = file_util::OpenFile(file, "r"); 257 SubDirInfo* current_info = NULL; 258 if (!fp) 259 return false; 260 261 // Read entries. 262 while (!feof(fp) && !ferror(fp)) { 263 std::string buf = ReadLine(fp); 264 if (buf == "") 265 break; 266 267 std::string entry; 268 TrimWhitespaceASCII(buf, TRIM_ALL, &entry); 269 if (entry.length() == 0 || entry[0] == '#') { 270 // Blank line or Comment. 271 continue; 272 } else if (entry[0] == '[' && info_array_) { 273 current_info = NULL; 274 std::string subdir = entry.substr(1, entry.length() - 2); 275 if (subdirs_.find(subdir) != subdirs_.end()) 276 current_info = &info_array_[subdirs_[subdir]]; 277 } 278 279 std::string key, value; 280 std::vector<std::string> r; 281 base::SplitStringDontTrim(entry, '=', &r); 282 if (r.size() < 2) 283 continue; 284 285 TrimWhitespaceASCII(r[0], TRIM_ALL, &key); 286 for (size_t i = 1; i < r.size(); i++) 287 value.append(r[i]); 288 TrimWhitespaceASCII(value, TRIM_ALL, &value); 289 290 if (current_info) { 291 if (key == "Size") { 292 current_info->size = atoi(value.c_str()); 293 } else if (key == "Type") { 294 if (value == "Fixed") 295 current_info->type = SubDirInfo::Fixed; 296 else if (value == "Scalable") 297 current_info->type = SubDirInfo::Scalable; 298 else if (value == "Threshold") 299 current_info->type = SubDirInfo::Threshold; 300 } else if (key == "MaxSize") { 301 current_info->max_size = atoi(value.c_str()); 302 } else if (key == "MinSize") { 303 current_info->min_size = atoi(value.c_str()); 304 } else if (key == "Threshold") { 305 current_info->threshold = atoi(value.c_str()); 306 } 307 } else { 308 if (key.compare("Directories") == 0 && !info_array_) { 309 if (!SetDirectories(value)) break; 310 } else if (key.compare("Inherits") == 0) { 311 if (value != "hicolor") 312 inherits_ = value; 313 } 314 } 315 } 316 317 file_util::CloseFile(fp); 318 return info_array_ != NULL; 319} 320 321size_t IconTheme::MatchesSize(SubDirInfo* info, size_t size) { 322 if (info->type == SubDirInfo::Fixed) { 323 return size - info->size; 324 } else if (info->type == SubDirInfo::Scalable) { 325 if (size >= info->min_size && size <= info->max_size) { 326 return 0; 327 } else { 328 return abs(size - info->min_size) < abs(size - info->max_size) ? 329 (size - info->min_size) : (size - info->max_size); 330 } 331 } else { 332 if (size >= info->size - info->threshold && 333 size <= info->size + info->threshold) { 334 return 0; 335 } else { 336 return abs(size - info->size - info->threshold) < 337 abs(size - info->size + info->threshold) 338 ? size - info->size - info->threshold 339 : size - info->size + info->threshold; 340 } 341 } 342} 343 344std::string IconTheme::ReadLine(FILE* fp) { 345 if (!fp) 346 return ""; 347 348 std::string result = ""; 349 const size_t kBufferSize = 100; 350 char buffer[kBufferSize]; 351 while ((fgets(buffer, kBufferSize - 1, fp)) != NULL) { 352 result += buffer; 353 size_t len = result.length(); 354 if (len == 0) 355 break; 356 char end = result[len - 1]; 357 if (end == '\n' || end == '\0') 358 break; 359 } 360 361 return result; 362} 363 364bool IconTheme::SetDirectories(const std::string& dirs) { 365 int num = 0; 366 std::string::size_type pos = 0, epos; 367 std::string dir; 368 while ((epos = dirs.find(',', pos)) != std::string::npos) { 369 TrimWhitespaceASCII(dirs.substr(pos, epos - pos), TRIM_ALL, &dir); 370 if (dir.length() == 0) { 371 LOG(WARNING) << "Invalid index.theme: blank subdir"; 372 return false; 373 } 374 subdirs_[dir] = num++; 375 pos = epos + 1; 376 } 377 TrimWhitespaceASCII(dirs.substr(pos), TRIM_ALL, &dir); 378 if (dir.length() == 0) { 379 LOG(WARNING) << "Invalid index.theme: blank subdir"; 380 return false; 381 } 382 subdirs_[dir] = num++; 383 info_array_ = new SubDirInfo[num]; 384 return true; 385} 386 387// Make sure |dir| exists and add it to the list of icon directories. 388void TryAddIconDir(const FilePath& dir) { 389 if (!file_util::DirectoryExists(dir)) 390 return; 391 (*MimeUtilConstants::GetInstance()->icon_dirs_)[dir] = 0; 392} 393 394// For a xdg directory |dir|, add the appropriate icon sub-directories. 395void AddXDGDataDir(const FilePath& dir) { 396 if (!file_util::DirectoryExists(dir)) 397 return; 398 TryAddIconDir(dir.Append("icons")); 399 TryAddIconDir(dir.Append("pixmaps")); 400} 401 402// Add all the xdg icon directories. 403void InitIconDir() { 404 MimeUtilConstants::GetInstance()->icon_dirs_->clear(); 405 FilePath home = file_util::GetHomeDir(); 406 if (!home.empty()) { 407 FilePath legacy_data_dir(home); 408 legacy_data_dir = legacy_data_dir.AppendASCII(".icons"); 409 if (file_util::DirectoryExists(legacy_data_dir)) 410 TryAddIconDir(legacy_data_dir); 411 } 412 const char* env = getenv("XDG_DATA_HOME"); 413 if (env) { 414 AddXDGDataDir(FilePath(env)); 415 } else if (!home.empty()) { 416 FilePath local_data_dir(home); 417 local_data_dir = local_data_dir.AppendASCII(".local"); 418 local_data_dir = local_data_dir.AppendASCII("share"); 419 AddXDGDataDir(local_data_dir); 420 } 421 422 env = getenv("XDG_DATA_DIRS"); 423 if (!env) { 424 AddXDGDataDir(FilePath("/usr/local/share")); 425 AddXDGDataDir(FilePath("/usr/share")); 426 } else { 427 std::string xdg_data_dirs = env; 428 std::string::size_type pos = 0, epos; 429 while ((epos = xdg_data_dirs.find(':', pos)) != std::string::npos) { 430 AddXDGDataDir(FilePath(xdg_data_dirs.substr(pos, epos - pos))); 431 pos = epos + 1; 432 } 433 AddXDGDataDir(FilePath(xdg_data_dirs.substr(pos))); 434 } 435} 436 437// Per xdg theme spec, we should check the icon directories every so often for 438// newly added icons. This isn't quite right. 439void EnsureUpdated() { 440 struct timeval t; 441 gettimeofday(&t, NULL); 442 time_t now = t.tv_sec; 443 MimeUtilConstants* constants = MimeUtilConstants::GetInstance(); 444 445 if (constants->last_check_time_ == 0) { 446 constants->icon_dirs_ = new std::map<FilePath, int>; 447 constants->icon_themes_ = new std::map<std::string, IconTheme*>; 448 InitIconDir(); 449 constants->last_check_time_ = now; 450 } else { 451 // TODO(thestig): something changed. start over. Upstream fix to Google 452 // Gadgets for Linux. 453 if (now > constants->last_check_time_ + constants->kUpdateInterval) { 454 } 455 } 456} 457 458// Find a fallback icon if we cannot find it in the default theme. 459FilePath LookupFallbackIcon(const std::string& icon_name) { 460 FilePath icon; 461 MimeUtilConstants* constants = MimeUtilConstants::GetInstance(); 462 std::map<FilePath, int>::iterator iter; 463 std::map<FilePath, int>* icon_dirs = constants->icon_dirs_; 464 std::vector<std::string>* icon_formats = &constants->icon_formats_; 465 for (iter = icon_dirs->begin(); iter != icon_dirs->end(); ++iter) { 466 for (size_t i = 0; i < icon_formats->size(); ++i) { 467 icon = iter->first.Append(icon_name + (*icon_formats)[i]); 468 if (file_util::PathExists(icon)) 469 return icon; 470 } 471 } 472 return FilePath(); 473} 474 475// Initialize the list of default themes. 476void InitDefaultThemes() { 477 IconTheme** default_themes = 478 MimeUtilConstants::GetInstance()->default_themes_; 479 480 char* env = getenv("KDE_FULL_SESSION"); 481 if (env) { 482 // KDE 483 std::string kde_default_theme; 484 std::string kde_fallback_theme; 485 486 // TODO(thestig): Figure out how to get the current icon theme on KDE. 487 // Setting stored in ~/.kde/share/config/kdeglobals under Icons -> Theme. 488 default_themes[0] = NULL; 489 490 // Try some reasonable defaults for KDE. 491 env = getenv("KDE_SESSION_VERSION"); 492 if (!env || env[0] != '4') { 493 // KDE 3 494 kde_default_theme = "default.kde"; 495 kde_fallback_theme = "crystalsvg"; 496 } else { 497 // KDE 4 498 kde_default_theme = "default.kde4"; 499 kde_fallback_theme = "oxygen"; 500 } 501 default_themes[1] = IconTheme::LoadTheme(kde_default_theme); 502 default_themes[2] = IconTheme::LoadTheme(kde_fallback_theme); 503 } else { 504 // Assume it's Gnome and use GTK to figure out the theme. 505 default_themes[1] = IconTheme::LoadTheme( 506 MimeUtilConstants::GetInstance()->gtk_theme_name_); 507 default_themes[2] = IconTheme::LoadTheme("gnome"); 508 } 509 // hicolor needs to be last per icon theme spec. 510 default_themes[3] = IconTheme::LoadTheme("hicolor"); 511 512 for (size_t i = 0; i < MimeUtilConstants::kDefaultThemeNum; i++) { 513 if (default_themes[i] == NULL) 514 continue; 515 // NULL out duplicate pointers. 516 for (size_t j = i + 1; j < MimeUtilConstants::kDefaultThemeNum; j++) { 517 if (default_themes[j] == default_themes[i]) 518 default_themes[j] = NULL; 519 } 520 } 521} 522 523// Try to find an icon with the name |icon_name| that's |size| pixels. 524FilePath LookupIconInDefaultTheme(const std::string& icon_name, int size) { 525 EnsureUpdated(); 526 MimeUtilConstants* constants = MimeUtilConstants::GetInstance(); 527 std::map<std::string, IconTheme*>* icon_themes = constants->icon_themes_; 528 if (icon_themes->size() == 0) 529 InitDefaultThemes(); 530 531 FilePath icon_path; 532 IconTheme** default_themes = constants->default_themes_; 533 for (size_t i = 0; i < MimeUtilConstants::kDefaultThemeNum; i++) { 534 if (default_themes[i]) { 535 icon_path = default_themes[i]->GetIconPath(icon_name, size, true); 536 if (!icon_path.empty()) 537 return icon_path; 538 } 539 } 540 return LookupFallbackIcon(icon_name); 541} 542 543MimeUtilConstants::~MimeUtilConstants() { 544 delete icon_dirs_; 545 delete icon_themes_; 546 for (size_t i = 0; i < kDefaultThemeNum; i++) 547 delete default_themes_[i]; 548} 549 550} // namespace 551 552namespace mime_util { 553 554std::string GetFileMimeType(const FilePath& filepath) { 555 base::ThreadRestrictions::AssertIOAllowed(); 556 return xdg_mime_get_mime_type_from_file_name(filepath.value().c_str()); 557} 558 559std::string GetDataMimeType(const std::string& data) { 560 base::ThreadRestrictions::AssertIOAllowed(); 561 return xdg_mime_get_mime_type_for_data(data.data(), data.length(), NULL); 562} 563 564void DetectGtkTheme() { 565 // If the theme name is already loaded, do nothing. Chrome doesn't respond 566 // to changes in the system theme, so we never need to set this more than 567 // once. 568 if (!MimeUtilConstants::GetInstance()->gtk_theme_name_.empty()) 569 return; 570 571 // We should only be called on the UI thread. 572 DCHECK_EQ(MessageLoop::TYPE_UI, MessageLoop::current()->type()); 573 574 gchar* gtk_theme_name; 575 g_object_get(gtk_settings_get_default(), 576 "gtk-icon-theme-name", 577 >k_theme_name, NULL); 578 MimeUtilConstants::GetInstance()->gtk_theme_name_.assign(gtk_theme_name); 579 g_free(gtk_theme_name); 580} 581 582FilePath GetMimeIcon(const std::string& mime_type, size_t size) { 583 base::ThreadRestrictions::AssertIOAllowed(); 584 std::vector<std::string> icon_names; 585 std::string icon_name; 586 FilePath icon_file; 587 588 const char* icon = xdg_mime_get_icon(mime_type.c_str()); 589 icon_name = std::string(icon ? icon : ""); 590 if (icon_name.length()) 591 icon_names.push_back(icon_name); 592 593 // For text/plain, try text-plain. 594 icon_name = mime_type; 595 for (size_t i = icon_name.find('/', 0); i != std::string::npos; 596 i = icon_name.find('/', i + 1)) { 597 icon_name[i] = '-'; 598 } 599 icon_names.push_back(icon_name); 600 // Also try gnome-mime-text-plain. 601 icon_names.push_back("gnome-mime-" + icon_name); 602 603 // Try "deb" for "application/x-deb" in KDE 3. 604 icon_name = mime_type.substr(mime_type.find("/x-") + 3); 605 icon_names.push_back(icon_name); 606 607 // Try generic name like text-x-generic. 608 icon_name = mime_type.substr(0, mime_type.find('/')) + "-x-generic"; 609 icon_names.push_back(icon_name); 610 611 // Last resort 612 icon_names.push_back("unknown"); 613 614 for (size_t i = 0; i < icon_names.size(); i++) { 615 if (icon_names[i][0] == '/') { 616 icon_file = FilePath(icon_names[i]); 617 if (file_util::PathExists(icon_file)) 618 return icon_file; 619 } else { 620 icon_file = LookupIconInDefaultTheme(icon_names[i], size); 621 if (!icon_file.empty()) 622 return icon_file; 623 } 624 } 625 return FilePath(); 626} 627 628} // namespace mime_util 629