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