OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 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 attempted_load = false; |
| 66 bool 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 |
| 80 if (attempted_load) |
| 81 return; |
| 82 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 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 } // namespace |
| 124 |
| 125 namespace libgtk2ui { |
| 126 |
| 127 AppIndicatorIcon::AppIndicatorIcon(std::string id) |
| 128 : id_(id), |
| 129 icon_(NULL), |
| 130 gtk_menu_(NULL), |
| 131 menu_model_(NULL), |
| 132 icon_change_count_(0), |
| 133 block_activation_(false), |
| 134 has_click_action_replacement_(false) { |
| 135 EnsureMethodsLoaded(); |
| 136 } |
| 137 AppIndicatorIcon::~AppIndicatorIcon() { |
| 138 if (icon_) { |
| 139 app_indicator_set_status(icon_, APP_INDICATOR_STATUS_PASSIVE); |
| 140 if (menu_model_) |
| 141 menu_model_->MenuClosed(); |
| 142 if (gtk_menu_) |
| 143 DestroyMenu(); |
| 144 g_object_unref(icon_); |
| 145 content::BrowserThread::GetBlockingPool()->PostTask( |
| 146 FROM_HERE, |
| 147 base::Bind(&AppIndicatorIcon::DeletePath, icon_file_path_.DirName())); |
| 148 } |
| 149 } |
| 150 |
| 151 bool AppIndicatorIcon::CouldOpen() { |
| 152 EnsureMethodsLoaded(); |
| 153 return opened; |
| 154 } |
| 155 |
| 156 void AppIndicatorIcon::SetImage(const gfx::ImageSkia& image) { |
| 157 if (opened) { |
| 158 ++icon_change_count_; |
| 159 gfx::ImageSkia safe_image = gfx::ImageSkia(image); |
| 160 safe_image.MakeThreadSafe(); |
| 161 base::PostTaskAndReplyWithResult( |
| 162 content::BrowserThread::GetBlockingPool() |
| 163 ->GetTaskRunnerWithShutdownBehavior( |
| 164 base::SequencedWorkerPool::SKIP_ON_SHUTDOWN).get(), |
| 165 FROM_HERE, |
| 166 base::Bind(&AppIndicatorIcon::CreateTempImageFile, |
| 167 safe_image, |
| 168 icon_change_count_, |
| 169 id_), |
| 170 base::Bind(&AppIndicatorIcon::SetImageFromFile, |
| 171 base::Unretained(this))); |
| 172 } |
| 173 } |
| 174 |
| 175 void AppIndicatorIcon::SetPressedImage(const gfx::ImageSkia& image) { |
| 176 // Ignore pressed images, since the standard on Linux is to not highlight |
| 177 // pressed status icons. |
| 178 } |
| 179 |
| 180 void AppIndicatorIcon::SetToolTip(const string16& tool_tip) { |
| 181 // App-indicators don't support tool-tips. Ignore call. |
| 182 } |
| 183 |
| 184 void AppIndicatorIcon::SetClickActionLabel(const string16& label) { |
| 185 click_action_label_ = UTF16ToUTF8(label); |
| 186 |
| 187 // If the menu item has already been created, then find the menu item and |
| 188 // change it's label. |
| 189 if (has_click_action_replacement_) { |
| 190 GList* children = gtk_container_get_children(GTK_CONTAINER(gtk_menu_)); |
| 191 for (GList* child = children; child; child = g_list_next(child)) |
| 192 if (g_object_get_data(G_OBJECT(child->data), "click-action-item") != |
| 193 NULL) { |
| 194 gtk_menu_item_set_label(GTK_MENU_ITEM(child->data), |
| 195 click_action_label_.c_str()); |
| 196 break; |
| 197 } |
| 198 g_list_free(children); |
| 199 } else if (icon_) { |
| 200 CreateClickActionReplacement(); |
| 201 app_indicator_set_menu(icon_, GTK_MENU(gtk_menu_)); |
| 202 } |
| 203 } |
| 204 |
| 205 void AppIndicatorIcon::UpdatePlatformContextMenu(ui::MenuModel* model) { |
| 206 if (!opened) |
| 207 return; |
| 208 |
| 209 if (gtk_menu_) { |
| 210 DestroyMenu(); |
| 211 has_click_action_replacement_ = false; |
| 212 } |
| 213 menu_model_ = model; |
| 214 |
| 215 // If icon doesn't exist now it's okay, the menu will be set later along with |
| 216 // the image. Both an icon and a menu are required to show an app indicator. |
| 217 if (model && icon_) |
| 218 SetMenu(); |
| 219 } |
| 220 |
| 221 void AppIndicatorIcon::SetImageFromFile(base::FilePath icon_file_path) { |
| 222 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| 223 if (!icon_file_path.empty()) { |
| 224 base::FilePath old_path = icon_file_path_; |
| 225 icon_file_path_ = icon_file_path; |
| 226 |
| 227 std::string icon_name = |
| 228 icon_file_path_.BaseName().RemoveExtension().value(); |
| 229 std::string icon_dir = icon_file_path_.DirName().value(); |
| 230 if (!icon_) { |
| 231 icon_ = |
| 232 app_indicator_new_with_path(id_.c_str(), |
| 233 icon_name.c_str(), |
| 234 APP_INDICATOR_CATEGORY_APPLICATION_STATUS, |
| 235 icon_dir.c_str()); |
| 236 app_indicator_set_status(icon_, APP_INDICATOR_STATUS_ACTIVE); |
| 237 if (menu_model_) { |
| 238 SetMenu(); |
| 239 } else if (!click_action_label_.empty()) { |
| 240 CreateClickActionReplacement(); |
| 241 app_indicator_set_menu(icon_, GTK_MENU(gtk_menu_)); |
| 242 } |
| 243 } else { |
| 244 // Currently we are creating a new temp directory every time the icon is |
| 245 // set. So we need to set the directory each time. |
| 246 app_indicator_set_icon_theme_path(icon_, icon_dir.c_str()); |
| 247 app_indicator_set_icon_full(icon_, icon_name.c_str(), "icon"); |
| 248 |
| 249 // Delete previous icon directory. |
| 250 content::BrowserThread::GetBlockingPool()->PostTask( |
| 251 FROM_HERE, |
| 252 base::Bind(&AppIndicatorIcon::DeletePath, old_path.DirName())); |
| 253 } |
| 254 } |
| 255 } |
| 256 |
| 257 void AppIndicatorIcon::SetMenu() { |
| 258 gtk_menu_ = gtk_menu_new(); |
| 259 BuildSubmenuFromModel(menu_model_, |
| 260 gtk_menu_, |
| 261 G_CALLBACK(OnMenuItemActivatedThunk), |
| 262 &block_activation_, |
| 263 this); |
| 264 if (!click_action_label_.empty()) |
| 265 CreateClickActionReplacement(); |
| 266 UpdateMenu(); |
| 267 menu_model_->MenuWillShow(); |
| 268 app_indicator_set_menu(icon_, GTK_MENU(gtk_menu_)); |
| 269 } |
| 270 |
| 271 void AppIndicatorIcon::CreateClickActionReplacement() { |
| 272 GtkWidget* menu_item = NULL; |
| 273 |
| 274 // If a menu doesn't exist create one just for the click action replacement. |
| 275 if (!gtk_menu_) { |
| 276 gtk_menu_ = gtk_menu_new(); |
| 277 } else { |
| 278 // Add separator before the other menu items. |
| 279 menu_item = gtk_separator_menu_item_new(); |
| 280 gtk_widget_show(menu_item); |
| 281 gtk_menu_shell_prepend(GTK_MENU_SHELL(gtk_menu_), menu_item); |
| 282 } |
| 283 |
| 284 // Add "click replacement menu item". |
| 285 menu_item = gtk_menu_item_new_with_mnemonic(click_action_label_.c_str()); |
| 286 g_object_set_data( |
| 287 G_OBJECT(menu_item), "click-action-item", GINT_TO_POINTER(1)); |
| 288 g_signal_connect(menu_item, "activate", G_CALLBACK(OnClickThunk), this); |
| 289 gtk_widget_show(menu_item); |
| 290 gtk_menu_shell_prepend(GTK_MENU_SHELL(gtk_menu_), menu_item); |
| 291 |
| 292 has_click_action_replacement_ = true; |
| 293 } |
| 294 |
| 295 void AppIndicatorIcon::DestroyMenu() { |
| 296 if (menu_model_) |
| 297 menu_model_->MenuClosed(); |
| 298 gtk_widget_destroy(gtk_menu_); |
| 299 gtk_menu_ = NULL; |
| 300 menu_model_ = NULL; |
| 301 } |
| 302 |
| 303 base::FilePath AppIndicatorIcon::CreateTempImageFile(gfx::ImageSkia image, |
| 304 int icon_change_count, |
| 305 std::string id) { |
| 306 scoped_refptr<base::RefCountedMemory> png_data = |
| 307 gfx::Image(image).As1xPNGBytes(); |
| 308 if (png_data->size() == 0) { |
| 309 // If the bitmap could not be encoded to PNG format, skip it. |
| 310 LOG(WARNING) << "Could not encode icon"; |
| 311 return base::FilePath(); |
| 312 } |
| 313 |
| 314 base::FilePath temp_dir; |
| 315 base::FilePath new_file_path; |
| 316 |
| 317 // Create a new temporary directory for each image since using a single |
| 318 // temporary directory seems to have issues when changing icons in quick |
| 319 // succession. |
| 320 if (!file_util::CreateNewTempDirectory("", &temp_dir)) |
| 321 return base::FilePath(); |
| 322 new_file_path = |
| 323 temp_dir.Append(id + base::StringPrintf("_%d.png", icon_change_count)); |
| 324 int bytes_written = |
| 325 file_util::WriteFile(new_file_path, |
| 326 reinterpret_cast<const char*>(png_data->front()), |
| 327 png_data->size()); |
| 328 |
| 329 if (bytes_written != static_cast<int>(png_data->size())) { |
| 330 return base::FilePath(); |
| 331 } |
| 332 |
| 333 return new_file_path; |
| 334 } |
| 335 |
| 336 void AppIndicatorIcon::DeletePath(base::FilePath icon_file_path) { |
| 337 if (!icon_file_path.empty()) { |
| 338 base::DeleteFile(icon_file_path, true); |
| 339 } |
| 340 } |
| 341 |
| 342 void AppIndicatorIcon::UpdateMenu() { |
| 343 gtk_container_foreach( |
| 344 GTK_CONTAINER(gtk_menu_), SetMenuItemInfo, &block_activation_); |
| 345 } |
| 346 |
| 347 void AppIndicatorIcon::OnClick(GtkWidget* menu_item) { |
| 348 if (delegate()) |
| 349 delegate()->OnClick(); |
| 350 } |
| 351 |
| 352 void AppIndicatorIcon::OnMenuItemActivated(GtkWidget* menu_item) { |
| 353 if (block_activation_) |
| 354 return; |
| 355 |
| 356 ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(menu_item)); |
| 357 |
| 358 if (!model) { |
| 359 // There won't be a model for "native" submenus like the "Input Methods" |
| 360 // context menu. We don't need to handle activation messages for submenus |
| 361 // anyway, so we can just return here. |
| 362 DCHECK(gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item))); |
| 363 return; |
| 364 } |
| 365 |
| 366 // The activate signal is sent to radio items as they get deselected; |
| 367 // ignore it in this case. |
| 368 if (GTK_IS_RADIO_MENU_ITEM(menu_item) && |
| 369 !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) { |
| 370 return; |
| 371 } |
| 372 |
| 373 int id; |
| 374 if (!GetMenuItemID(menu_item, &id)) |
| 375 return; |
| 376 |
| 377 // The menu item can still be activated by hotkeys even if it is disabled. |
| 378 if (menu_model_->IsEnabledAt(id)) |
| 379 ExecuteCommand(model, id); |
| 380 UpdateMenu(); |
| 381 } |
| 382 |
| 383 } // namespace libgtk2ui |
OLD | NEW |