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.
5 #include "chrome/browser/ui/libgtk2ui/app_indicator_icon.h"
10 #include "base/bind.h"
11 #include "base/environment.h"
12 #include "base/files/file_util.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 "third_party/skia/include/core/SkBitmap.h"
22 #include "third_party/skia/include/core/SkCanvas.h"
23 #include "ui/base/models/menu_model.h"
24 #include "ui/gfx/codec/png_codec.h"
25 #include "ui/gfx/image/image.h"
26 #include "ui/gfx/image/image_skia.h"
31 APP_INDICATOR_CATEGORY_APPLICATION_STATUS
,
32 APP_INDICATOR_CATEGORY_COMMUNICATIONS
,
33 APP_INDICATOR_CATEGORY_SYSTEM_SERVICES
,
34 APP_INDICATOR_CATEGORY_HARDWARE
,
35 APP_INDICATOR_CATEGORY_OTHER
36 } AppIndicatorCategory
;
39 APP_INDICATOR_STATUS_PASSIVE
,
40 APP_INDICATOR_STATUS_ACTIVE
,
41 APP_INDICATOR_STATUS_ATTENTION
44 typedef AppIndicator
* (*app_indicator_new_func
)(const gchar
* id
,
45 const gchar
* icon_name
,
46 AppIndicatorCategory category
);
48 typedef AppIndicator
* (*app_indicator_new_with_path_func
)(
50 const gchar
* icon_name
,
51 AppIndicatorCategory category
,
52 const gchar
* icon_theme_path
);
54 typedef void (*app_indicator_set_status_func
)(AppIndicator
* self
,
55 AppIndicatorStatus status
);
57 typedef void (*app_indicator_set_attention_icon_full_func
)(
59 const gchar
* icon_name
,
60 const gchar
* icon_desc
);
62 typedef void (*app_indicator_set_menu_func
)(AppIndicator
* self
, GtkMenu
* menu
);
64 typedef void (*app_indicator_set_icon_full_func
)(AppIndicator
* self
,
65 const gchar
* icon_name
,
66 const gchar
* icon_desc
);
68 typedef void (*app_indicator_set_icon_theme_path_func
)(
70 const gchar
* icon_theme_path
);
72 bool g_attempted_load
= false;
73 bool g_opened
= false;
75 // Retrieved functions from libappindicator.
76 app_indicator_new_func app_indicator_new
= NULL
;
77 app_indicator_new_with_path_func app_indicator_new_with_path
= NULL
;
78 app_indicator_set_status_func app_indicator_set_status
= NULL
;
79 app_indicator_set_attention_icon_full_func
80 app_indicator_set_attention_icon_full
= NULL
;
81 app_indicator_set_menu_func app_indicator_set_menu
= NULL
;
82 app_indicator_set_icon_full_func app_indicator_set_icon_full
= NULL
;
83 app_indicator_set_icon_theme_path_func app_indicator_set_icon_theme_path
= NULL
;
85 void EnsureMethodsLoaded() {
89 g_attempted_load
= true;
91 // Only use libappindicator where it is needed to support dbus based status
92 // icons. In particular, libappindicator does not support a click action.
93 scoped_ptr
<base::Environment
> env(base::Environment::Create());
94 base::nix::DesktopEnvironment environment
=
95 base::nix::GetDesktopEnvironment(env
.get());
96 if (environment
!= base::nix::DESKTOP_ENVIRONMENT_KDE4
&&
97 environment
!= base::nix::DESKTOP_ENVIRONMENT_UNITY
) {
101 void* indicator_lib
= dlopen("libappindicator.so", RTLD_LAZY
);
102 if (!indicator_lib
) {
103 indicator_lib
= dlopen("libappindicator.so.1", RTLD_LAZY
);
105 if (!indicator_lib
) {
106 indicator_lib
= dlopen("libappindicator.so.0", RTLD_LAZY
);
108 if (!indicator_lib
) {
114 app_indicator_new
= reinterpret_cast<app_indicator_new_func
>(
115 dlsym(indicator_lib
, "app_indicator_new"));
117 app_indicator_new_with_path
=
118 reinterpret_cast<app_indicator_new_with_path_func
>(
119 dlsym(indicator_lib
, "app_indicator_new_with_path"));
121 app_indicator_set_status
= reinterpret_cast<app_indicator_set_status_func
>(
122 dlsym(indicator_lib
, "app_indicator_set_status"));
124 app_indicator_set_attention_icon_full
=
125 reinterpret_cast<app_indicator_set_attention_icon_full_func
>(
126 dlsym(indicator_lib
, "app_indicator_set_attention_icon_full"));
128 app_indicator_set_menu
= reinterpret_cast<app_indicator_set_menu_func
>(
129 dlsym(indicator_lib
, "app_indicator_set_menu"));
131 app_indicator_set_icon_full
=
132 reinterpret_cast<app_indicator_set_icon_full_func
>(
133 dlsym(indicator_lib
, "app_indicator_set_icon_full"));
135 app_indicator_set_icon_theme_path
=
136 reinterpret_cast<app_indicator_set_icon_theme_path_func
>(
137 dlsym(indicator_lib
, "app_indicator_set_icon_theme_path"));
140 // Writes |bitmap| to a file at |path|. Returns true if successful.
141 bool WriteFile(const base::FilePath
& path
, const SkBitmap
& bitmap
) {
142 std::vector
<unsigned char> png_data
;
143 if (!gfx::PNGCodec::EncodeBGRASkBitmap(bitmap
, false, &png_data
))
145 int bytes_written
= base::WriteFile(
146 path
, reinterpret_cast<char*>(&png_data
[0]), png_data
.size());
147 return (bytes_written
== static_cast<int>(png_data
.size()));
150 void DeleteTempDirectory(const base::FilePath
& dir_path
) {
151 if (dir_path
.empty())
153 base::DeleteFile(dir_path
, true);
158 namespace libgtk2ui
{
160 AppIndicatorIcon::AppIndicatorIcon(std::string id
,
161 const gfx::ImageSkia
& image
,
162 const base::string16
& tool_tip
)
167 icon_change_count_(0),
168 weak_factory_(this) {
169 scoped_ptr
<base::Environment
> env(base::Environment::Create());
170 using_kde4_
= base::nix::GetDesktopEnvironment(env
.get()) ==
171 base::nix::DESKTOP_ENVIRONMENT_KDE4
;
173 EnsureMethodsLoaded();
174 tool_tip_
= base::UTF16ToUTF8(tool_tip
);
177 AppIndicatorIcon::~AppIndicatorIcon() {
179 app_indicator_set_status(icon_
, APP_INDICATOR_STATUS_PASSIVE
);
180 g_object_unref(icon_
);
181 content::BrowserThread::GetBlockingPool()->PostTask(
183 base::Bind(&DeleteTempDirectory
, temp_dir_
));
188 bool AppIndicatorIcon::CouldOpen() {
189 EnsureMethodsLoaded();
193 void AppIndicatorIcon::SetImage(const gfx::ImageSkia
& image
) {
197 ++icon_change_count_
;
199 // Copy the bitmap because it may be freed by the time it's accessed in
201 SkBitmap safe_bitmap
= *image
.bitmap();
203 scoped_refptr
<base::TaskRunner
> task_runner
=
204 content::BrowserThread::GetBlockingPool()
205 ->GetTaskRunnerWithShutdownBehavior(
206 base::SequencedWorkerPool::SKIP_ON_SHUTDOWN
);
208 base::PostTaskAndReplyWithResult(
209 task_runner
.get(), FROM_HERE
,
210 base::Bind(AppIndicatorIcon::WriteKDE4TempImageOnWorkerThread
,
211 safe_bitmap
, temp_dir_
),
212 base::Bind(&AppIndicatorIcon::SetImageFromFile
,
213 weak_factory_
.GetWeakPtr()));
215 base::PostTaskAndReplyWithResult(
216 task_runner
.get(), FROM_HERE
,
217 base::Bind(AppIndicatorIcon::WriteUnityTempImageOnWorkerThread
,
218 safe_bitmap
, icon_change_count_
, id_
),
219 base::Bind(&AppIndicatorIcon::SetImageFromFile
,
220 weak_factory_
.GetWeakPtr()));
224 void AppIndicatorIcon::SetToolTip(const base::string16
& tool_tip
) {
225 DCHECK(!tool_tip_
.empty());
226 tool_tip_
= base::UTF16ToUTF8(tool_tip
);
227 UpdateClickActionReplacementMenuItem();
230 void AppIndicatorIcon::UpdatePlatformContextMenu(ui::MenuModel
* model
) {
236 // The icon is created asynchronously so it might not exist when the menu is
242 void AppIndicatorIcon::RefreshPlatformContextMenu() {
247 AppIndicatorIcon::SetImageFromFileParams
248 AppIndicatorIcon::WriteKDE4TempImageOnWorkerThread(
249 const SkBitmap
& bitmap
,
250 const base::FilePath
& existing_temp_dir
) {
251 base::FilePath temp_dir
= existing_temp_dir
;
252 if (temp_dir
.empty() &&
253 !base::CreateNewTempDirectory(base::FilePath::StringType(), &temp_dir
)) {
254 LOG(WARNING
) << "Could not create temporary directory";
255 return SetImageFromFileParams();
258 base::FilePath icon_theme_path
= temp_dir
.AppendASCII("icons");
260 // On KDE4, an image located in a directory ending with
261 // "icons/hicolor/24x24/apps" can be used as the app indicator image because
262 // "/usr/share/icons/hicolor/24x24/apps" exists.
263 base::FilePath image_dir
= icon_theme_path
.AppendASCII("hicolor")
264 .AppendASCII("24x24")
265 .AppendASCII("apps");
267 if (!base::CreateDirectory(image_dir
))
268 return SetImageFromFileParams();
270 // On KDE4, the name of the image file for each different looking bitmap must
271 // be unique. It must also be unique across runs of Chrome.
272 std::vector
<unsigned char> bitmap_png_data
;
273 if (!gfx::PNGCodec::EncodeBGRASkBitmap(bitmap
, false, &bitmap_png_data
)) {
274 LOG(WARNING
) << "Could not encode icon";
275 return SetImageFromFileParams();
277 base::MD5Digest digest
;
278 base::MD5Sum(reinterpret_cast<char*>(&bitmap_png_data
[0]),
279 bitmap_png_data
.size(), &digest
);
280 std::string icon_name
= base::StringPrintf(
281 "chrome_app_indicator2_%s", base::MD5DigestToBase16(digest
).c_str());
283 // If |bitmap| is not 24x24, KDE does some really ugly resizing. Pad |bitmap|
284 // with transparent pixels to make it 24x24.
285 const int kDesiredSize
= 24;
286 SkBitmap scaled_bitmap
;
287 scaled_bitmap
.allocN32Pixels(kDesiredSize
, kDesiredSize
);
288 scaled_bitmap
.eraseARGB(0, 0, 0, 0);
289 SkCanvas
canvas(scaled_bitmap
);
290 canvas
.drawBitmap(bitmap
, (kDesiredSize
- bitmap
.width()) / 2,
291 (kDesiredSize
- bitmap
.height()) / 2);
293 base::FilePath image_path
= image_dir
.Append(icon_name
+ ".png");
294 if (!WriteFile(image_path
, scaled_bitmap
))
295 return SetImageFromFileParams();
297 SetImageFromFileParams params
;
298 params
.parent_temp_dir
= temp_dir
;
299 params
.icon_theme_path
= icon_theme_path
.value();
300 params
.icon_name
= icon_name
;
305 AppIndicatorIcon::SetImageFromFileParams
306 AppIndicatorIcon::WriteUnityTempImageOnWorkerThread(const SkBitmap
& bitmap
,
307 int icon_change_count
,
308 const std::string
& id
) {
309 // Create a new temporary directory for each image on Unity since using a
310 // single temporary directory seems to have issues when changing icons in
312 base::FilePath temp_dir
;
313 if (!base::CreateNewTempDirectory(base::FilePath::StringType(), &temp_dir
)) {
314 LOG(WARNING
) << "Could not create temporary directory";
315 return SetImageFromFileParams();
318 std::string icon_name
=
319 base::StringPrintf("%s_%d", id
.c_str(), icon_change_count
);
320 base::FilePath image_path
= temp_dir
.Append(icon_name
+ ".png");
321 SetImageFromFileParams params
;
322 if (WriteFile(image_path
, bitmap
)) {
323 params
.parent_temp_dir
= temp_dir
;
324 params
.icon_theme_path
= temp_dir
.value();
325 params
.icon_name
= icon_name
;
330 void AppIndicatorIcon::SetImageFromFile(const SetImageFromFileParams
& params
) {
331 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
332 if (params
.icon_theme_path
.empty())
337 app_indicator_new_with_path(id_
.c_str(),
338 params
.icon_name
.c_str(),
339 APP_INDICATOR_CATEGORY_APPLICATION_STATUS
,
340 params
.icon_theme_path
.c_str());
341 app_indicator_set_status(icon_
, APP_INDICATOR_STATUS_ACTIVE
);
344 app_indicator_set_icon_theme_path(icon_
, params
.icon_theme_path
.c_str());
345 app_indicator_set_icon_full(icon_
, params
.icon_name
.c_str(), "icon");
348 if (temp_dir_
!= params
.parent_temp_dir
) {
349 content::BrowserThread::GetBlockingPool()->PostTask(
351 base::Bind(&DeleteTempDirectory
, temp_dir_
));
352 temp_dir_
= params
.parent_temp_dir
;
356 void AppIndicatorIcon::SetMenu() {
357 menu_
.reset(new AppIndicatorIconMenu(menu_model_
));
358 UpdateClickActionReplacementMenuItem();
359 app_indicator_set_menu(icon_
, menu_
->GetGtkMenu());
362 void AppIndicatorIcon::UpdateClickActionReplacementMenuItem() {
363 // The menu may not have been created yet.
367 if (!delegate()->HasClickAction() && menu_model_
)
370 DCHECK(!tool_tip_
.empty());
371 menu_
->UpdateClickActionReplacementMenuItem(
373 base::Bind(&AppIndicatorIcon::OnClickActionReplacementMenuItemActivated
,
374 base::Unretained(this)));
377 void AppIndicatorIcon::OnClickActionReplacementMenuItemActivated() {
379 delegate()->OnClick();
382 } // namespace libgtk2ui