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 <gtk/gtk.h>
      8 #include <dlfcn.h>
      9 
     10 #include "base/bind.h"
     11 #include "base/file_util.h"
     12 #include "base/memory/ref_counted_memory.h"
     13 #include "base/strings/stringprintf.h"
     14 #include "base/strings/utf_string_conversions.h"
     15 #include "base/threading/sequenced_worker_pool.h"
     16 #include "chrome/browser/ui/libgtk2ui/menu_util.h"
     17 #include "content/public/browser/browser_thread.h"
     18 #include "ui/base/models/menu_model.h"
     19 #include "ui/gfx/image/image_skia.h"
     20 
     21 namespace {
     22 
     23 typedef enum {
     24   APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
     25   APP_INDICATOR_CATEGORY_COMMUNICATIONS,
     26   APP_INDICATOR_CATEGORY_SYSTEM_SERVICES,
     27   APP_INDICATOR_CATEGORY_HARDWARE,
     28   APP_INDICATOR_CATEGORY_OTHER
     29 } AppIndicatorCategory;
     30 
     31 typedef enum {
     32   APP_INDICATOR_STATUS_PASSIVE,
     33   APP_INDICATOR_STATUS_ACTIVE,
     34   APP_INDICATOR_STATUS_ATTENTION
     35 } AppIndicatorStatus;
     36 
     37 typedef AppIndicator* (*app_indicator_new_func)(const gchar* id,
     38                                                 const gchar* icon_name,
     39                                                 AppIndicatorCategory category);
     40 
     41 typedef AppIndicator* (*app_indicator_new_with_path_func)(
     42     const gchar* id,
     43     const gchar* icon_name,
     44     AppIndicatorCategory category,
     45     const gchar* icon_theme_path);
     46 
     47 typedef void (*app_indicator_set_status_func)(AppIndicator* self,
     48                                               AppIndicatorStatus status);
     49 
     50 typedef void (*app_indicator_set_attention_icon_full_func)(
     51     AppIndicator* self,
     52     const gchar* icon_name,
     53     const gchar* icon_desc);
     54 
     55 typedef void (*app_indicator_set_menu_func)(AppIndicator* self, GtkMenu* menu);
     56 
     57 typedef void (*app_indicator_set_icon_full_func)(AppIndicator* self,
     58                                                  const gchar* icon_name,
     59                                                  const gchar* icon_desc);
     60 
     61 typedef void (*app_indicator_set_icon_theme_path_func)(
     62     AppIndicator* self,
     63     const gchar* icon_theme_path);
     64 
     65 bool g_attempted_load = false;
     66 bool g_opened = false;
     67 
     68 // Retrieved functions from libappindicator.
     69 app_indicator_new_func app_indicator_new = NULL;
     70 app_indicator_new_with_path_func app_indicator_new_with_path = NULL;
     71 app_indicator_set_status_func app_indicator_set_status = NULL;
     72 app_indicator_set_attention_icon_full_func
     73     app_indicator_set_attention_icon_full = NULL;
     74 app_indicator_set_menu_func app_indicator_set_menu = NULL;
     75 app_indicator_set_icon_full_func app_indicator_set_icon_full = NULL;
     76 app_indicator_set_icon_theme_path_func app_indicator_set_icon_theme_path = NULL;
     77 
     78 void EnsureMethodsLoaded() {
     79   if (g_attempted_load)
     80     return;
     81 
     82   g_attempted_load = true;
     83 
     84   void* indicator_lib = dlopen("libappindicator.so", RTLD_LAZY);
     85   if (!indicator_lib) {
     86     indicator_lib = dlopen("libappindicator.so.1", RTLD_LAZY);
     87   }
     88   if (!indicator_lib) {
     89     indicator_lib = dlopen("libappindicator.so.0", RTLD_LAZY);
     90   }
     91   if (!indicator_lib) {
     92     return;
     93   }
     94 
     95   g_opened = true;
     96 
     97   app_indicator_new = reinterpret_cast<app_indicator_new_func>(
     98       dlsym(indicator_lib, "app_indicator_new"));
     99 
    100   app_indicator_new_with_path =
    101       reinterpret_cast<app_indicator_new_with_path_func>(
    102           dlsym(indicator_lib, "app_indicator_new_with_path"));
    103 
    104   app_indicator_set_status = reinterpret_cast<app_indicator_set_status_func>(
    105       dlsym(indicator_lib, "app_indicator_set_status"));
    106 
    107   app_indicator_set_attention_icon_full =
    108       reinterpret_cast<app_indicator_set_attention_icon_full_func>(
    109           dlsym(indicator_lib, "app_indicator_set_attention_icon_full"));
    110 
    111   app_indicator_set_menu = reinterpret_cast<app_indicator_set_menu_func>(
    112       dlsym(indicator_lib, "app_indicator_set_menu"));
    113 
    114   app_indicator_set_icon_full =
    115       reinterpret_cast<app_indicator_set_icon_full_func>(
    116           dlsym(indicator_lib, "app_indicator_set_icon_full"));
    117 
    118   app_indicator_set_icon_theme_path =
    119       reinterpret_cast<app_indicator_set_icon_theme_path_func>(
    120           dlsym(indicator_lib, "app_indicator_set_icon_theme_path"));
    121 }
    122 
    123 base::FilePath CreateTempImageFile(gfx::ImageSkia* image_ptr,
    124                                    int icon_change_count,
    125                                    std::string id) {
    126   scoped_ptr<gfx::ImageSkia> image(image_ptr);
    127 
    128   scoped_refptr<base::RefCountedMemory> png_data =
    129       gfx::Image(*image.get()).As1xPNGBytes();
    130   if (png_data->size() == 0) {
    131     // If the bitmap could not be encoded to PNG format, skip it.
    132     LOG(WARNING) << "Could not encode icon";
    133     return base::FilePath();
    134   }
    135 
    136   base::FilePath temp_dir;
    137   base::FilePath new_file_path;
    138 
    139   // Create a new temporary directory for each image since using a single
    140   // temporary directory seems to have issues when changing icons in quick
    141   // succession.
    142   if (!base::CreateNewTempDirectory(base::FilePath::StringType(), &temp_dir))
    143     return base::FilePath();
    144   new_file_path =
    145       temp_dir.Append(id + base::StringPrintf("_%d.png", icon_change_count));
    146   int bytes_written =
    147       file_util::WriteFile(new_file_path,
    148                            reinterpret_cast<const char*>(png_data->front()),
    149                            png_data->size());
    150 
    151   if (bytes_written != static_cast<int>(png_data->size()))
    152     return base::FilePath();
    153   return new_file_path;
    154 }
    155 
    156 void DeleteTempImagePath(const base::FilePath& icon_file_path) {
    157   if (icon_file_path.empty())
    158     return;
    159   base::DeleteFile(icon_file_path, true);
    160 }
    161 
    162 }  // namespace
    163 
    164 namespace libgtk2ui {
    165 
    166 AppIndicatorIcon::AppIndicatorIcon(std::string id,
    167                                    const gfx::ImageSkia& image,
    168                                    const base::string16& tool_tip)
    169     : id_(id),
    170       icon_(NULL),
    171       gtk_menu_(NULL),
    172       menu_model_(NULL),
    173       icon_change_count_(0),
    174       block_activation_(false),
    175       weak_factory_(this) {
    176   EnsureMethodsLoaded();
    177   tool_tip_ = UTF16ToUTF8(tool_tip);
    178   SetImage(image);
    179 }
    180 AppIndicatorIcon::~AppIndicatorIcon() {
    181   if (icon_) {
    182     app_indicator_set_status(icon_, APP_INDICATOR_STATUS_PASSIVE);
    183     if (gtk_menu_)
    184       DestroyMenu();
    185     g_object_unref(icon_);
    186     content::BrowserThread::GetBlockingPool()->PostTask(
    187         FROM_HERE,
    188         base::Bind(&DeleteTempImagePath, icon_file_path_.DirName()));
    189   }
    190 }
    191 
    192 // static
    193 bool AppIndicatorIcon::CouldOpen() {
    194   EnsureMethodsLoaded();
    195   return g_opened;
    196 }
    197 
    198 void AppIndicatorIcon::SetImage(const gfx::ImageSkia& image) {
    199   if (!g_opened)
    200     return;
    201 
    202   ++icon_change_count_;
    203 
    204   // We create a deep copy of the image since it may have been freed by the time
    205   // it's accessed in the other thread.
    206   scoped_ptr<gfx::ImageSkia> safe_image(image.DeepCopy());
    207   base::PostTaskAndReplyWithResult(
    208       content::BrowserThread::GetBlockingPool()
    209           ->GetTaskRunnerWithShutdownBehavior(
    210                 base::SequencedWorkerPool::SKIP_ON_SHUTDOWN).get(),
    211       FROM_HERE,
    212       base::Bind(&CreateTempImageFile,
    213                  safe_image.release(),
    214                  icon_change_count_,
    215                  id_),
    216       base::Bind(&AppIndicatorIcon::SetImageFromFile,
    217                  weak_factory_.GetWeakPtr()));
    218 }
    219 
    220 void AppIndicatorIcon::SetPressedImage(const gfx::ImageSkia& image) {
    221   // Ignore pressed images, since the standard on Linux is to not highlight
    222   // pressed status icons.
    223 }
    224 
    225 void AppIndicatorIcon::SetToolTip(const base::string16& tool_tip) {
    226   DCHECK(!tool_tip_.empty());
    227   tool_tip_ = UTF16ToUTF8(tool_tip);
    228 
    229   // We can set the click action label only if the icon exists. Also we only
    230   // need to update the label if it is shown and it's only shown if we are sure
    231   // that there is a click action or if there is no menu.
    232   if (icon_ && (delegate()->HasClickAction() || menu_model_ == NULL)) {
    233     GList* children = gtk_container_get_children(GTK_CONTAINER(gtk_menu_));
    234     for (GList* child = children; child; child = g_list_next(child))
    235       if (g_object_get_data(G_OBJECT(child->data), "click-action-item") !=
    236           NULL) {
    237         gtk_menu_item_set_label(GTK_MENU_ITEM(child->data),
    238                                 tool_tip_.c_str());
    239         break;
    240       }
    241     g_list_free(children);
    242   }
    243 }
    244 
    245 void AppIndicatorIcon::UpdatePlatformContextMenu(ui::MenuModel* model) {
    246   if (!g_opened)
    247     return;
    248 
    249   if (gtk_menu_) {
    250     DestroyMenu();
    251   }
    252   menu_model_ = model;
    253 
    254   // The icon is created asynchronously so it might not exist when the menu is
    255   // set.
    256   if (icon_)
    257     SetMenu();
    258 }
    259 
    260 void AppIndicatorIcon::RefreshPlatformContextMenu() {
    261   gtk_container_foreach(
    262       GTK_CONTAINER(gtk_menu_), SetMenuItemInfo, &block_activation_);
    263 }
    264 
    265 void AppIndicatorIcon::SetImageFromFile(const base::FilePath& icon_file_path) {
    266   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    267   if (icon_file_path.empty())
    268     return;
    269 
    270   base::FilePath old_path = icon_file_path_;
    271   icon_file_path_ = icon_file_path;
    272 
    273   std::string icon_name =
    274       icon_file_path_.BaseName().RemoveExtension().value();
    275   std::string icon_dir = icon_file_path_.DirName().value();
    276   if (!icon_) {
    277     icon_ =
    278         app_indicator_new_with_path(id_.c_str(),
    279                                     icon_name.c_str(),
    280                                     APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
    281                                     icon_dir.c_str());
    282     app_indicator_set_status(icon_, APP_INDICATOR_STATUS_ACTIVE);
    283     SetMenu();
    284   } else {
    285     // Currently we are creating a new temp directory every time the icon is
    286     // set. So we need to set the directory each time.
    287     app_indicator_set_icon_theme_path(icon_, icon_dir.c_str());
    288     app_indicator_set_icon_full(icon_, icon_name.c_str(), "icon");
    289 
    290     // Delete previous icon directory.
    291     content::BrowserThread::GetBlockingPool()->PostTask(
    292         FROM_HERE,
    293         base::Bind(&DeleteTempImagePath, old_path.DirName()));
    294   }
    295 }
    296 
    297 void AppIndicatorIcon::SetMenu() {
    298   gtk_menu_ = gtk_menu_new();
    299 
    300   if (delegate()->HasClickAction() || menu_model_ == NULL) {
    301     CreateClickActionReplacement();
    302     if (menu_model_) {
    303       // Add separator before the other menu items.
    304       GtkWidget* menu_item = gtk_separator_menu_item_new();
    305       gtk_widget_show(menu_item);
    306       gtk_menu_shell_append(GTK_MENU_SHELL(gtk_menu_), menu_item);
    307     }
    308   }
    309   if (menu_model_) {
    310     BuildSubmenuFromModel(menu_model_,
    311                           gtk_menu_,
    312                           G_CALLBACK(OnMenuItemActivatedThunk),
    313                           &block_activation_,
    314                           this);
    315     RefreshPlatformContextMenu();
    316   }
    317   app_indicator_set_menu(icon_, GTK_MENU(gtk_menu_));
    318 }
    319 
    320 void AppIndicatorIcon::CreateClickActionReplacement() {
    321   DCHECK(!tool_tip_.empty());
    322 
    323   // Add "click replacement menu item".
    324   GtkWidget* menu_item = gtk_menu_item_new_with_mnemonic(tool_tip_.c_str());
    325   g_object_set_data(
    326       G_OBJECT(menu_item), "click-action-item", GINT_TO_POINTER(1));
    327   g_signal_connect(menu_item, "activate", G_CALLBACK(OnClickThunk), this);
    328   gtk_widget_show(menu_item);
    329   gtk_menu_shell_prepend(GTK_MENU_SHELL(gtk_menu_), menu_item);
    330 }
    331 
    332 void AppIndicatorIcon::DestroyMenu() {
    333   gtk_widget_destroy(gtk_menu_);
    334   gtk_menu_ = NULL;
    335   menu_model_ = NULL;
    336 }
    337 
    338 void AppIndicatorIcon::OnClick(GtkWidget* menu_item) {
    339   if (delegate())
    340     delegate()->OnClick();
    341 }
    342 
    343 void AppIndicatorIcon::OnMenuItemActivated(GtkWidget* menu_item) {
    344   if (block_activation_)
    345     return;
    346 
    347   ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(menu_item));
    348   if (!model) {
    349     // There won't be a model for "native" submenus like the "Input Methods"
    350     // context menu. We don't need to handle activation messages for submenus
    351     // anyway, so we can just return here.
    352     DCHECK(gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)));
    353     return;
    354   }
    355 
    356   // The activate signal is sent to radio items as they get deselected;
    357   // ignore it in this case.
    358   if (GTK_IS_RADIO_MENU_ITEM(menu_item) &&
    359       !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) {
    360     return;
    361   }
    362 
    363   int id;
    364   if (!GetMenuItemID(menu_item, &id))
    365     return;
    366 
    367   // The menu item can still be activated by hotkeys even if it is disabled.
    368   if (menu_model_->IsEnabledAt(id))
    369     ExecuteCommand(model, id);
    370 }
    371 
    372 }  // namespace libgtk2ui
    373