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