Home | History | Annotate | Download | only in libgtk2ui
      1 // Copyright 2013 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 "chrome/browser/ui/libgtk2ui/app_indicator_icon.h"
      6 
      7 #include <dlfcn.h>
      8 #include <gtk/gtk.h>
      9 
     10 #include "base/bind.h"
     11 #include "base/environment.h"
     12 #include "base/files/file_util.h"
     13 #include "base/md5.h"
     14 #include "base/memory/ref_counted_memory.h"
     15 #include "base/nix/xdg_util.h"
     16 #include "base/strings/stringprintf.h"
     17 #include "base/strings/utf_string_conversions.h"
     18 #include "base/threading/sequenced_worker_pool.h"
     19 #include "chrome/browser/ui/libgtk2ui/app_indicator_icon_menu.h"
     20 #include "content/public/browser/browser_thread.h"
     21 #include "ui/base/models/menu_model.h"
     22 #include "ui/gfx/image/image.h"
     23 #include "ui/gfx/image/image_skia.h"
     24 
     25 namespace {
     26 
     27 typedef enum {
     28   APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
     29   APP_INDICATOR_CATEGORY_COMMUNICATIONS,
     30   APP_INDICATOR_CATEGORY_SYSTEM_SERVICES,
     31   APP_INDICATOR_CATEGORY_HARDWARE,
     32   APP_INDICATOR_CATEGORY_OTHER
     33 } AppIndicatorCategory;
     34 
     35 typedef enum {
     36   APP_INDICATOR_STATUS_PASSIVE,
     37   APP_INDICATOR_STATUS_ACTIVE,
     38   APP_INDICATOR_STATUS_ATTENTION
     39 } AppIndicatorStatus;
     40 
     41 typedef AppIndicator* (*app_indicator_new_func)(const gchar* id,
     42                                                 const gchar* icon_name,
     43                                                 AppIndicatorCategory category);
     44 
     45 typedef AppIndicator* (*app_indicator_new_with_path_func)(
     46     const gchar* id,
     47     const gchar* icon_name,
     48     AppIndicatorCategory category,
     49     const gchar* icon_theme_path);
     50 
     51 typedef void (*app_indicator_set_status_func)(AppIndicator* self,
     52                                               AppIndicatorStatus status);
     53 
     54 typedef void (*app_indicator_set_attention_icon_full_func)(
     55     AppIndicator* self,
     56     const gchar* icon_name,
     57     const gchar* icon_desc);
     58 
     59 typedef void (*app_indicator_set_menu_func)(AppIndicator* self, GtkMenu* menu);
     60 
     61 typedef void (*app_indicator_set_icon_full_func)(AppIndicator* self,
     62                                                  const gchar* icon_name,
     63                                                  const gchar* icon_desc);
     64 
     65 typedef void (*app_indicator_set_icon_theme_path_func)(
     66     AppIndicator* self,
     67     const gchar* icon_theme_path);
     68 
     69 bool g_attempted_load = false;
     70 bool g_opened = false;
     71 
     72 // Retrieved functions from libappindicator.
     73 app_indicator_new_func app_indicator_new = NULL;
     74 app_indicator_new_with_path_func app_indicator_new_with_path = NULL;
     75 app_indicator_set_status_func app_indicator_set_status = NULL;
     76 app_indicator_set_attention_icon_full_func
     77     app_indicator_set_attention_icon_full = NULL;
     78 app_indicator_set_menu_func app_indicator_set_menu = NULL;
     79 app_indicator_set_icon_full_func app_indicator_set_icon_full = NULL;
     80 app_indicator_set_icon_theme_path_func app_indicator_set_icon_theme_path = NULL;
     81 
     82 void EnsureMethodsLoaded() {
     83   if (g_attempted_load)
     84     return;
     85 
     86   g_attempted_load = true;
     87 
     88   // Only use libappindicator where it is needed to support dbus based status
     89   // icons. In particular, libappindicator does not support a click action.
     90   scoped_ptr<base::Environment> env(base::Environment::Create());
     91   base::nix::DesktopEnvironment environment =
     92       base::nix::GetDesktopEnvironment(env.get());
     93   if (environment != base::nix::DESKTOP_ENVIRONMENT_KDE4 &&
     94       environment != base::nix::DESKTOP_ENVIRONMENT_UNITY) {
     95     return;
     96   }
     97 
     98   void* indicator_lib = dlopen("libappindicator.so", RTLD_LAZY);
     99   if (!indicator_lib) {
    100     indicator_lib = dlopen("libappindicator.so.1", RTLD_LAZY);
    101   }
    102   if (!indicator_lib) {
    103     indicator_lib = dlopen("libappindicator.so.0", RTLD_LAZY);
    104   }
    105   if (!indicator_lib) {
    106     return;
    107   }
    108 
    109   g_opened = true;
    110 
    111   app_indicator_new = reinterpret_cast<app_indicator_new_func>(
    112       dlsym(indicator_lib, "app_indicator_new"));
    113 
    114   app_indicator_new_with_path =
    115       reinterpret_cast<app_indicator_new_with_path_func>(
    116           dlsym(indicator_lib, "app_indicator_new_with_path"));
    117 
    118   app_indicator_set_status = reinterpret_cast<app_indicator_set_status_func>(
    119       dlsym(indicator_lib, "app_indicator_set_status"));
    120 
    121   app_indicator_set_attention_icon_full =
    122       reinterpret_cast<app_indicator_set_attention_icon_full_func>(
    123           dlsym(indicator_lib, "app_indicator_set_attention_icon_full"));
    124 
    125   app_indicator_set_menu = reinterpret_cast<app_indicator_set_menu_func>(
    126       dlsym(indicator_lib, "app_indicator_set_menu"));
    127 
    128   app_indicator_set_icon_full =
    129       reinterpret_cast<app_indicator_set_icon_full_func>(
    130           dlsym(indicator_lib, "app_indicator_set_icon_full"));
    131 
    132   app_indicator_set_icon_theme_path =
    133       reinterpret_cast<app_indicator_set_icon_theme_path_func>(
    134           dlsym(indicator_lib, "app_indicator_set_icon_theme_path"));
    135 }
    136 
    137 // Returns whether a temporary directory should be created for each app
    138 // indicator image.
    139 bool ShouldCreateTempDirectoryPerImage(bool using_kde4) {
    140   // Create a new temporary directory for each image on Unity since using a
    141   // single temporary directory seems to have issues when changing icons in
    142   // quick succession.
    143   return !using_kde4;
    144 }
    145 
    146 // Returns the subdirectory of |temp_dir| in which the app indicator image
    147 // should be saved.
    148 base::FilePath GetImageDirectoryPath(bool using_kde4,
    149                                      const base::FilePath& temp_dir) {
    150   // On KDE4, an image located in a directory ending with
    151   // "icons/hicolor/16x16/apps" can be used as the app indicator image because
    152   // "/usr/share/icons/hicolor/16x16/apps" exists.
    153   return using_kde4 ?
    154       temp_dir.AppendASCII("icons").AppendASCII("hicolor").AppendASCII("16x16").
    155           AppendASCII("apps") :
    156       temp_dir;
    157 }
    158 
    159 std::string GetImageFileNameForKDE4(
    160     const scoped_refptr<base::RefCountedMemory>& png_data) {
    161   // On KDE4, the name of the image file for each different looking bitmap must
    162   // be unique. It must also be unique across runs of Chrome.
    163   base::MD5Digest digest;
    164   base::MD5Sum(png_data->front_as<char>(), png_data->size(), &digest);
    165   return base::StringPrintf("chrome_app_indicator_%s.png",
    166                             base::MD5DigestToBase16(digest).c_str());
    167 }
    168 
    169 std::string GetImageFileNameForNonKDE4(int icon_change_count,
    170                                        const std::string& id) {
    171   return base::StringPrintf("%s_%d.png", id.c_str(), icon_change_count);
    172 }
    173 
    174 // Returns the "icon theme path" given the file path of the app indicator image.
    175 std::string GetIconThemePath(bool using_kde4,
    176                              const base::FilePath& image_path) {
    177   return using_kde4 ?
    178       image_path.DirName().DirName().DirName().DirName().value() :
    179       image_path.DirName().value();
    180 }
    181 
    182 base::FilePath CreateTempImageFile(bool using_kde4,
    183                                    gfx::ImageSkia* image_ptr,
    184                                    int icon_change_count,
    185                                    std::string id,
    186                                    const base::FilePath& previous_file_path) {
    187   scoped_ptr<gfx::ImageSkia> image(image_ptr);
    188 
    189   scoped_refptr<base::RefCountedMemory> png_data =
    190       gfx::Image(*image.get()).As1xPNGBytes();
    191   if (png_data->size() == 0) {
    192     // If the bitmap could not be encoded to PNG format, skip it.
    193     LOG(WARNING) << "Could not encode icon";
    194     return base::FilePath();
    195   }
    196 
    197   base::FilePath new_file_path;
    198   if (previous_file_path.empty() ||
    199       ShouldCreateTempDirectoryPerImage(using_kde4)) {
    200     base::FilePath tmp_dir;
    201     if (!base::CreateNewTempDirectory(base::FilePath::StringType(), &tmp_dir))
    202       return base::FilePath();
    203     new_file_path = GetImageDirectoryPath(using_kde4, tmp_dir);
    204     if (new_file_path != tmp_dir) {
    205       if (!base::CreateDirectory(new_file_path))
    206         return base::FilePath();
    207     }
    208   } else {
    209     new_file_path = previous_file_path.DirName();
    210   }
    211 
    212   new_file_path = new_file_path.Append(using_kde4 ?
    213       GetImageFileNameForKDE4(png_data) :
    214       GetImageFileNameForNonKDE4(icon_change_count, id));
    215 
    216   int bytes_written =
    217       base::WriteFile(new_file_path,
    218                       png_data->front_as<char>(), png_data->size());
    219 
    220   if (bytes_written != static_cast<int>(png_data->size()))
    221     return base::FilePath();
    222   return new_file_path;
    223 }
    224 
    225 void DeleteTempDirectory(const base::FilePath& dir_path) {
    226   if (dir_path.empty())
    227     return;
    228   base::DeleteFile(dir_path, true);
    229 }
    230 
    231 }  // namespace
    232 
    233 namespace libgtk2ui {
    234 
    235 AppIndicatorIcon::AppIndicatorIcon(std::string id,
    236                                    const gfx::ImageSkia& image,
    237                                    const base::string16& tool_tip)
    238     : id_(id),
    239       using_kde4_(false),
    240       icon_(NULL),
    241       menu_model_(NULL),
    242       icon_change_count_(0),
    243       weak_factory_(this) {
    244   scoped_ptr<base::Environment> env(base::Environment::Create());
    245   using_kde4_ = base::nix::GetDesktopEnvironment(env.get()) ==
    246       base::nix::DESKTOP_ENVIRONMENT_KDE4;
    247 
    248   EnsureMethodsLoaded();
    249   tool_tip_ = base::UTF16ToUTF8(tool_tip);
    250   SetImage(image);
    251 }
    252 AppIndicatorIcon::~AppIndicatorIcon() {
    253   if (icon_) {
    254     app_indicator_set_status(icon_, APP_INDICATOR_STATUS_PASSIVE);
    255     g_object_unref(icon_);
    256     content::BrowserThread::GetBlockingPool()->PostTask(
    257         FROM_HERE,
    258         base::Bind(&DeleteTempDirectory, icon_file_path_.DirName()));
    259   }
    260 }
    261 
    262 // static
    263 bool AppIndicatorIcon::CouldOpen() {
    264   EnsureMethodsLoaded();
    265   return g_opened;
    266 }
    267 
    268 void AppIndicatorIcon::SetImage(const gfx::ImageSkia& image) {
    269   if (!g_opened)
    270     return;
    271 
    272   ++icon_change_count_;
    273 
    274   // We create a deep copy of the image since it may have been freed by the time
    275   // it's accessed in the other thread.
    276   scoped_ptr<gfx::ImageSkia> safe_image(image.DeepCopy());
    277   base::PostTaskAndReplyWithResult(
    278       content::BrowserThread::GetBlockingPool()
    279           ->GetTaskRunnerWithShutdownBehavior(
    280                 base::SequencedWorkerPool::SKIP_ON_SHUTDOWN).get(),
    281       FROM_HERE,
    282       base::Bind(&CreateTempImageFile,
    283                  using_kde4_,
    284                  safe_image.release(),
    285                  icon_change_count_,
    286                  id_,
    287                  icon_file_path_),
    288       base::Bind(&AppIndicatorIcon::SetImageFromFile,
    289                  weak_factory_.GetWeakPtr()));
    290 }
    291 
    292 void AppIndicatorIcon::SetToolTip(const base::string16& tool_tip) {
    293   DCHECK(!tool_tip_.empty());
    294   tool_tip_ = base::UTF16ToUTF8(tool_tip);
    295   UpdateClickActionReplacementMenuItem();
    296 }
    297 
    298 void AppIndicatorIcon::UpdatePlatformContextMenu(ui::MenuModel* model) {
    299   if (!g_opened)
    300     return;
    301 
    302   menu_model_ = model;
    303 
    304   // The icon is created asynchronously so it might not exist when the menu is
    305   // set.
    306   if (icon_)
    307     SetMenu();
    308 }
    309 
    310 void AppIndicatorIcon::RefreshPlatformContextMenu() {
    311   menu_->Refresh();
    312 }
    313 
    314 void AppIndicatorIcon::SetImageFromFile(const base::FilePath& icon_file_path) {
    315   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    316   if (icon_file_path.empty())
    317     return;
    318 
    319   base::FilePath old_path = icon_file_path_;
    320   icon_file_path_ = icon_file_path;
    321 
    322   std::string icon_name =
    323       icon_file_path_.BaseName().RemoveExtension().value();
    324   std::string icon_dir = GetIconThemePath(using_kde4_, icon_file_path);
    325   if (!icon_) {
    326     icon_ =
    327         app_indicator_new_with_path(id_.c_str(),
    328                                     icon_name.c_str(),
    329                                     APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
    330                                     icon_dir.c_str());
    331     app_indicator_set_status(icon_, APP_INDICATOR_STATUS_ACTIVE);
    332     SetMenu();
    333   } else {
    334     // Currently we are creating a new temp directory every time the icon is
    335     // set. So we need to set the directory each time.
    336     app_indicator_set_icon_theme_path(icon_, icon_dir.c_str());
    337     app_indicator_set_icon_full(icon_, icon_name.c_str(), "icon");
    338 
    339     if (ShouldCreateTempDirectoryPerImage(using_kde4_)) {
    340       // Delete previous icon directory.
    341       content::BrowserThread::GetBlockingPool()->PostTask(
    342           FROM_HERE,
    343           base::Bind(&DeleteTempDirectory, old_path.DirName()));
    344     }
    345   }
    346 }
    347 
    348 void AppIndicatorIcon::SetMenu() {
    349   menu_.reset(new AppIndicatorIconMenu(menu_model_));
    350   UpdateClickActionReplacementMenuItem();
    351   app_indicator_set_menu(icon_, menu_->GetGtkMenu());
    352 }
    353 
    354 void AppIndicatorIcon::UpdateClickActionReplacementMenuItem() {
    355   // The menu may not have been created yet.
    356   if (!menu_.get())
    357     return;
    358 
    359   if (!delegate()->HasClickAction() && menu_model_)
    360     return;
    361 
    362   DCHECK(!tool_tip_.empty());
    363   menu_->UpdateClickActionReplacementMenuItem(
    364       tool_tip_.c_str(),
    365       base::Bind(&AppIndicatorIcon::OnClickActionReplacementMenuItemActivated,
    366                  base::Unretained(this)));
    367 }
    368 
    369 void AppIndicatorIcon::OnClickActionReplacementMenuItemActivated() {
    370   if (delegate())
    371     delegate()->OnClick();
    372 }
    373 
    374 }  // namespace libgtk2ui
    375