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 "ui/base/models/menu_model.h"
22 #include "ui/gfx/image/image.h"
23 #include "ui/gfx/image/image_skia.h"
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
;
36 APP_INDICATOR_STATUS_PASSIVE
,
37 APP_INDICATOR_STATUS_ACTIVE
,
38 APP_INDICATOR_STATUS_ATTENTION
41 typedef AppIndicator
* (*app_indicator_new_func
)(const gchar
* id
,
42 const gchar
* icon_name
,
43 AppIndicatorCategory category
);
45 typedef AppIndicator
* (*app_indicator_new_with_path_func
)(
47 const gchar
* icon_name
,
48 AppIndicatorCategory category
,
49 const gchar
* icon_theme_path
);
51 typedef void (*app_indicator_set_status_func
)(AppIndicator
* self
,
52 AppIndicatorStatus status
);
54 typedef void (*app_indicator_set_attention_icon_full_func
)(
56 const gchar
* icon_name
,
57 const gchar
* icon_desc
);
59 typedef void (*app_indicator_set_menu_func
)(AppIndicator
* self
, GtkMenu
* menu
);
61 typedef void (*app_indicator_set_icon_full_func
)(AppIndicator
* self
,
62 const gchar
* icon_name
,
63 const gchar
* icon_desc
);
65 typedef void (*app_indicator_set_icon_theme_path_func
)(
67 const gchar
* icon_theme_path
);
69 bool g_attempted_load
= false;
70 bool g_opened
= false;
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
;
82 void EnsureMethodsLoaded() {
86 g_attempted_load
= true;
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
) {
98 void* indicator_lib
= dlopen("libappindicator.so", RTLD_LAZY
);
100 indicator_lib
= dlopen("libappindicator.so.1", RTLD_LAZY
);
102 if (!indicator_lib
) {
103 indicator_lib
= dlopen("libappindicator.so.0", RTLD_LAZY
);
105 if (!indicator_lib
) {
111 app_indicator_new
= reinterpret_cast<app_indicator_new_func
>(
112 dlsym(indicator_lib
, "app_indicator_new"));
114 app_indicator_new_with_path
=
115 reinterpret_cast<app_indicator_new_with_path_func
>(
116 dlsym(indicator_lib
, "app_indicator_new_with_path"));
118 app_indicator_set_status
= reinterpret_cast<app_indicator_set_status_func
>(
119 dlsym(indicator_lib
, "app_indicator_set_status"));
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"));
125 app_indicator_set_menu
= reinterpret_cast<app_indicator_set_menu_func
>(
126 dlsym(indicator_lib
, "app_indicator_set_menu"));
128 app_indicator_set_icon_full
=
129 reinterpret_cast<app_indicator_set_icon_full_func
>(
130 dlsym(indicator_lib
, "app_indicator_set_icon_full"));
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"));
137 // Returns whether a temporary directory should be created for each app
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
146 // Returns the subdirectory of |temp_dir| in which the app indicator image
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.
154 temp_dir
.AppendASCII("icons").AppendASCII("hicolor").AppendASCII("16x16").
155 AppendASCII("apps") :
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());
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
);
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
) {
178 image_path
.DirName().DirName().DirName().DirName().value() :
179 image_path
.DirName().value();
182 base::FilePath
CreateTempImageFile(bool using_kde4
,
183 gfx::ImageSkia
* image_ptr
,
184 int icon_change_count
,
186 const base::FilePath
& previous_file_path
) {
187 scoped_ptr
<gfx::ImageSkia
> image(image_ptr
);
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();
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();
209 new_file_path
= previous_file_path
.DirName();
212 new_file_path
= new_file_path
.Append(using_kde4
?
213 GetImageFileNameForKDE4(png_data
) :
214 GetImageFileNameForNonKDE4(icon_change_count
, id
));
217 base::WriteFile(new_file_path
,
218 png_data
->front_as
<char>(), png_data
->size());
220 if (bytes_written
!= static_cast<int>(png_data
->size()))
221 return base::FilePath();
222 return new_file_path
;
225 void DeleteTempDirectory(const base::FilePath
& dir_path
) {
226 if (dir_path
.empty())
228 base::DeleteFile(dir_path
, true);
233 namespace libgtk2ui
{
235 AppIndicatorIcon::AppIndicatorIcon(std::string id
,
236 const gfx::ImageSkia
& image
,
237 const base::string16
& tool_tip
)
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
;
248 EnsureMethodsLoaded();
249 tool_tip_
= base::UTF16ToUTF8(tool_tip
);
252 AppIndicatorIcon::~AppIndicatorIcon() {
254 app_indicator_set_status(icon_
, APP_INDICATOR_STATUS_PASSIVE
);
255 g_object_unref(icon_
);
256 content::BrowserThread::GetBlockingPool()->PostTask(
258 base::Bind(&DeleteTempDirectory
, icon_file_path_
.DirName()));
263 bool AppIndicatorIcon::CouldOpen() {
264 EnsureMethodsLoaded();
268 void AppIndicatorIcon::SetImage(const gfx::ImageSkia
& image
) {
272 ++icon_change_count_
;
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(),
282 base::Bind(&CreateTempImageFile
,
284 safe_image
.release(),
288 base::Bind(&AppIndicatorIcon::SetImageFromFile
,
289 weak_factory_
.GetWeakPtr()));
292 void AppIndicatorIcon::SetToolTip(const base::string16
& tool_tip
) {
293 DCHECK(!tool_tip_
.empty());
294 tool_tip_
= base::UTF16ToUTF8(tool_tip
);
295 UpdateClickActionReplacementMenuItem();
298 void AppIndicatorIcon::UpdatePlatformContextMenu(ui::MenuModel
* model
) {
304 // The icon is created asynchronously so it might not exist when the menu is
310 void AppIndicatorIcon::RefreshPlatformContextMenu() {
314 void AppIndicatorIcon::SetImageFromFile(const base::FilePath
& icon_file_path
) {
315 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
316 if (icon_file_path
.empty())
319 base::FilePath old_path
= icon_file_path_
;
320 icon_file_path_
= icon_file_path
;
322 std::string icon_name
=
323 icon_file_path_
.BaseName().RemoveExtension().value();
324 std::string icon_dir
= GetIconThemePath(using_kde4_
, icon_file_path
);
327 app_indicator_new_with_path(id_
.c_str(),
329 APP_INDICATOR_CATEGORY_APPLICATION_STATUS
,
331 app_indicator_set_status(icon_
, APP_INDICATOR_STATUS_ACTIVE
);
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");
339 if (ShouldCreateTempDirectoryPerImage(using_kde4_
)) {
340 // Delete previous icon directory.
341 content::BrowserThread::GetBlockingPool()->PostTask(
343 base::Bind(&DeleteTempDirectory
, old_path
.DirName()));
348 void AppIndicatorIcon::SetMenu() {
349 menu_
.reset(new AppIndicatorIconMenu(menu_model_
));
350 UpdateClickActionReplacementMenuItem();
351 app_indicator_set_menu(icon_
, menu_
->GetGtkMenu());
354 void AppIndicatorIcon::UpdateClickActionReplacementMenuItem() {
355 // The menu may not have been created yet.
359 if (!delegate()->HasClickAction() && menu_model_
)
362 DCHECK(!tool_tip_
.empty());
363 menu_
->UpdateClickActionReplacementMenuItem(
365 base::Bind(&AppIndicatorIcon::OnClickActionReplacementMenuItemActivated
,
366 base::Unretained(this)));
369 void AppIndicatorIcon::OnClickActionReplacementMenuItemActivated() {
371 delegate()->OnClick();
374 } // namespace libgtk2ui