diff --git a/atom.gyp b/atom.gyp index 3c95bf0904..e3075b4d98 100644 --- a/atom.gyp +++ b/atom.gyp @@ -24,6 +24,7 @@ 'atom/browser/api/lib/menu-item.coffee', 'atom/browser/api/lib/power-monitor.coffee', 'atom/browser/api/lib/protocol.coffee', + 'atom/browser/api/lib/tray.coffee', 'atom/browser/api/lib/web-contents.coffee', 'atom/browser/lib/init.coffee', 'atom/browser/lib/objects-registry.coffee', @@ -62,6 +63,8 @@ 'atom/browser/api/atom_api_power_monitor.h', 'atom/browser/api/atom_api_protocol.cc', 'atom/browser/api/atom_api_protocol.h', + 'atom/browser/api/atom_api_tray.cc', + 'atom/browser/api/atom_api_tray.h', 'atom/browser/api/atom_api_web_contents.cc', 'atom/browser/api/atom_api_web_contents.h', 'atom/browser/api/atom_api_window.cc', @@ -127,14 +130,29 @@ 'atom/browser/ui/file_dialog_gtk.cc', 'atom/browser/ui/file_dialog_mac.mm', 'atom/browser/ui/file_dialog_win.cc', + 'atom/browser/ui/gtk/app_indicator_icon.cc', + 'atom/browser/ui/gtk/app_indicator_icon.h', + 'atom/browser/ui/gtk/status_icon.cc', + 'atom/browser/ui/gtk/status_icon.h', 'atom/browser/ui/message_box.h', 'atom/browser/ui/message_box_gtk.cc', 'atom/browser/ui/message_box_mac.mm', 'atom/browser/ui/message_box_win.cc', + 'atom/browser/ui/tray_icon.cc', + 'atom/browser/ui/tray_icon.h', + 'atom/browser/ui/tray_icon_gtk.cc', + 'atom/browser/ui/tray_icon_cocoa.h', + 'atom/browser/ui/tray_icon_cocoa.mm', + 'atom/browser/ui/tray_icon_observer.h', + 'atom/browser/ui/tray_icon_win.cc', 'atom/browser/ui/win/menu_2.cc', 'atom/browser/ui/win/menu_2.h', 'atom/browser/ui/win/native_menu_win.cc', 'atom/browser/ui/win/native_menu_win.h', + 'atom/browser/ui/win/notify_icon_host.cc', + 'atom/browser/ui/win/notify_icon_host.h', + 'atom/browser/ui/win/notify_icon.cc', + 'atom/browser/ui/win/notify_icon.h', 'atom/browser/window_list.cc', 'atom/browser/window_list.h', 'atom/browser/window_list_observer.h', @@ -176,6 +194,8 @@ 'atom/common/native_mate_converters/file_path_converter.h', 'atom/common/native_mate_converters/function_converter.h', 'atom/common/native_mate_converters/gurl_converter.h', + 'atom/common/native_mate_converters/image_converter.cc', + 'atom/common/native_mate_converters/image_converter.h', 'atom/common/native_mate_converters/string16_converter.h', 'atom/common/native_mate_converters/v8_value_converter.cc', 'atom/common/native_mate_converters/v8_value_converter.h', @@ -215,6 +235,8 @@ 'chrome/browser/ui/gtk/gtk_window_util.h', 'chrome/browser/ui/gtk/menu_gtk.cc', 'chrome/browser/ui/gtk/menu_gtk.h', + 'chrome/browser/ui/views/status_icons/status_tray_state_changer_win.cc', + 'chrome/browser/ui/views/status_icons/status_tray_state_changer_win.h', '<@(native_mate_files)', ], 'framework_sources': [ diff --git a/atom/browser/api/atom_api_menu.h b/atom/browser/api/atom_api_menu.h index a309e1b1eb..2a212d0145 100644 --- a/atom/browser/api/atom_api_menu.h +++ b/atom/browser/api/atom_api_menu.h @@ -34,6 +34,8 @@ class Menu : public mate::Wrappable, static void SendActionToFirstResponder(const std::string& action); #endif + ui::SimpleMenuModel* model() const { return model_.get(); } + protected: Menu(); virtual ~Menu(); diff --git a/atom/browser/api/atom_api_tray.cc b/atom/browser/api/atom_api_tray.cc new file mode 100644 index 0000000000..49d9540244 --- /dev/null +++ b/atom/browser/api/atom_api_tray.cc @@ -0,0 +1,83 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/api/atom_api_tray.h" + +#include + +#include "atom/browser/api/atom_api_menu.h" +#include "atom/browser/ui/tray_icon.h" +#include "atom/common/native_mate_converters/image_converter.h" +#include "native_mate/constructor.h" +#include "native_mate/dictionary.h" + +#include "atom/common/node_includes.h" + +namespace atom { + +namespace api { + +Tray::Tray(const gfx::ImageSkia& image) + : tray_icon_(TrayIcon::Create()) { + tray_icon_->SetImage(image); + tray_icon_->AddObserver(this); +} + +Tray::~Tray() { +} + +// static +mate::Wrappable* Tray::New(const gfx::ImageSkia& image) { + return new Tray(image); +} + +void Tray::OnClicked() { + Emit("clicked"); +} + +void Tray::SetImage(const gfx::ImageSkia& image) { + tray_icon_->SetImage(image); +} + +void Tray::SetPressedImage(const gfx::ImageSkia& image) { + tray_icon_->SetPressedImage(image); +} + +void Tray::SetToolTip(const std::string& tool_tip) { + tray_icon_->SetToolTip(tool_tip); +} + +void Tray::SetContextMenu(Menu* menu) { + tray_icon_->SetContextMenu(menu->model()); +} + +// static +void Tray::BuildPrototype(v8::Isolate* isolate, + v8::Handle prototype) { + mate::ObjectTemplateBuilder(isolate, prototype) + .SetMethod("setImage", &Tray::SetImage) + .SetMethod("setPressedImage", &Tray::SetPressedImage) + .SetMethod("setToolTip", &Tray::SetToolTip) + .SetMethod("_setContextMenu", &Tray::SetContextMenu); +} + +} // namespace api + +} // namespace atom + + +namespace { + +void Initialize(v8::Handle exports) { + using atom::api::Tray; + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Handle constructor = mate::CreateConstructor( + isolate, "Tray", base::Bind(&Tray::New)); + mate::Dictionary dict(isolate, exports); + dict.Set("Tray", static_cast>(constructor)); +} + +} // namespace + +NODE_MODULE(atom_browser_tray, Initialize) diff --git a/atom/browser/api/atom_api_tray.h b/atom/browser/api/atom_api_tray.h new file mode 100644 index 0000000000..5dce0cbe95 --- /dev/null +++ b/atom/browser/api/atom_api_tray.h @@ -0,0 +1,56 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_ATOM_API_TRAY_H_ +#define ATOM_BROWSER_API_ATOM_API_TRAY_H_ + +#include + +#include "atom/browser/api/event_emitter.h" +#include "atom/browser/ui/tray_icon_observer.h" +#include "base/memory/scoped_ptr.h" + +namespace gfx { +class ImageSkia; +} + +namespace atom { + +class TrayIcon; + +namespace api { + +class Menu; + +class Tray : public mate::EventEmitter, + public TrayIconObserver { + public: + static mate::Wrappable* New(const gfx::ImageSkia& image); + + static void BuildPrototype(v8::Isolate* isolate, + v8::Handle prototype); + + protected: + explicit Tray(const gfx::ImageSkia& image); + virtual ~Tray(); + + // TrayIcon implementations: + virtual void OnClicked() OVERRIDE; + + void SetImage(const gfx::ImageSkia& image); + void SetPressedImage(const gfx::ImageSkia& image); + void SetToolTip(const std::string& tool_tip); + void SetContextMenu(Menu* menu); + + private: + scoped_ptr tray_icon_; + + DISALLOW_COPY_AND_ASSIGN(Tray); +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_ATOM_API_TRAY_H_ diff --git a/atom/browser/api/lib/tray.coffee b/atom/browser/api/lib/tray.coffee new file mode 100644 index 0000000000..7d158a9a01 --- /dev/null +++ b/atom/browser/api/lib/tray.coffee @@ -0,0 +1,10 @@ +EventEmitter = require('events').EventEmitter +bindings = process.atomBinding 'tray' + +Tray = bindings.Tray +Tray::__proto__ = EventEmitter.prototype +Tray::setContextMenu = (menu) -> + @_setContextMenu menu + @menu = menu # Keep a strong reference of menu. + +module.exports = Tray diff --git a/atom/browser/ui/gtk/app_indicator_icon.cc b/atom/browser/ui/gtk/app_indicator_icon.cc new file mode 100644 index 0000000000..3b39ea0fc9 --- /dev/null +++ b/atom/browser/ui/gtk/app_indicator_icon.cc @@ -0,0 +1,258 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/gtk/app_indicator_icon.h" + +#include +#include + +#include "base/file_util.h" +#include "base/guid.h" +#include "base/memory/ref_counted_memory.h" +#include "base/strings/stringprintf.h" +#include "base/threading/sequenced_worker_pool.h" +#include "chrome/browser/ui/gtk/menu_gtk.h" +#include "content/public/browser/browser_thread.h" +#include "ui/gfx/image/image.h" + +namespace { + +typedef enum { + APP_INDICATOR_CATEGORY_APPLICATION_STATUS, + APP_INDICATOR_CATEGORY_COMMUNICATIONS, + APP_INDICATOR_CATEGORY_SYSTEM_SERVICES, + APP_INDICATOR_CATEGORY_HARDWARE, + APP_INDICATOR_CATEGORY_OTHER +} AppIndicatorCategory; + +typedef enum { + APP_INDICATOR_STATUS_PASSIVE, + APP_INDICATOR_STATUS_ACTIVE, + APP_INDICATOR_STATUS_ATTENTION +} AppIndicatorStatus; + +typedef AppIndicator* (*app_indicator_new_func)(const gchar* id, + const gchar* icon_name, + AppIndicatorCategory category); + +typedef AppIndicator* (*app_indicator_new_with_path_func)( + const gchar* id, + const gchar* icon_name, + AppIndicatorCategory category, + const gchar* icon_theme_path); + +typedef void (*app_indicator_set_status_func)(AppIndicator* self, + AppIndicatorStatus status); + +typedef void (*app_indicator_set_attention_icon_full_func)( + AppIndicator* self, + const gchar* icon_name, + const gchar* icon_desc); + +typedef void (*app_indicator_set_menu_func)(AppIndicator* self, GtkMenu* menu); + +typedef void (*app_indicator_set_icon_full_func)(AppIndicator* self, + const gchar* icon_name, + const gchar* icon_desc); + +typedef void (*app_indicator_set_icon_theme_path_func)( + AppIndicator* self, + const gchar* icon_theme_path); + +bool g_attempted_load = false; +bool g_opened = false; + +// Retrieved functions from libappindicator. +app_indicator_new_func app_indicator_new = NULL; +app_indicator_new_with_path_func app_indicator_new_with_path = NULL; +app_indicator_set_status_func app_indicator_set_status = NULL; +app_indicator_set_attention_icon_full_func + app_indicator_set_attention_icon_full = NULL; +app_indicator_set_menu_func app_indicator_set_menu = NULL; +app_indicator_set_icon_full_func app_indicator_set_icon_full = NULL; +app_indicator_set_icon_theme_path_func app_indicator_set_icon_theme_path = NULL; + +void EnsureMethodsLoaded() { + if (g_attempted_load) + return; + + g_attempted_load = true; + + void* indicator_lib = dlopen("libappindicator.so", RTLD_LAZY); + if (!indicator_lib) { + indicator_lib = dlopen("libappindicator.so.1", RTLD_LAZY); + } + if (!indicator_lib) { + indicator_lib = dlopen("libappindicator.so.0", RTLD_LAZY); + } + if (!indicator_lib) { + return; + } + + g_opened = true; + + app_indicator_new = reinterpret_cast( + dlsym(indicator_lib, "app_indicator_new")); + + app_indicator_new_with_path = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_new_with_path")); + + app_indicator_set_status = reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_status")); + + app_indicator_set_attention_icon_full = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_attention_icon_full")); + + app_indicator_set_menu = reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_menu")); + + app_indicator_set_icon_full = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_icon_full")); + + app_indicator_set_icon_theme_path = + reinterpret_cast( + dlsym(indicator_lib, "app_indicator_set_icon_theme_path")); +} + +base::FilePath CreateTempImageFile(gfx::ImageSkia* image_ptr, + int icon_change_count, + std::string id) { + scoped_ptr image(image_ptr); + + scoped_refptr png_data = + gfx::Image(*image.get()).As1xPNGBytes(); + if (png_data->size() == 0) { + // If the bitmap could not be encoded to PNG format, skip it. + LOG(WARNING) << "Could not encode icon"; + return base::FilePath(); + } + + base::FilePath temp_dir; + base::FilePath new_file_path; + + // Create a new temporary directory for each image since using a single + // temporary directory seems to have issues when changing icons in quick + // succession. + if (!file_util::CreateNewTempDirectory(base::FilePath::StringType(), + &temp_dir)) + return base::FilePath(); + new_file_path = + temp_dir.Append(id + base::StringPrintf("_%d.png", icon_change_count)); + int bytes_written = + file_util::WriteFile( + new_file_path, + reinterpret_cast(png_data->front()), + png_data->size()); + + if (bytes_written != static_cast(png_data->size())) + return base::FilePath(); + return new_file_path; +} + +void DeleteTempImagePath(const base::FilePath& icon_file_path) { + if (icon_file_path.empty()) + return; + base::DeleteFile(icon_file_path, true); +} + +} // namespace + +namespace atom { + +AppIndicatorIcon::AppIndicatorIcon() + : icon_(NULL), + id_(base::GenerateGUID()), + icon_change_count_(0), + weak_factory_(this) { +} + +AppIndicatorIcon::~AppIndicatorIcon() { + if (icon_) { + app_indicator_set_status(icon_, APP_INDICATOR_STATUS_PASSIVE); + // if (gtk_menu_) + // DestroyMenu(); + g_object_unref(icon_); + content::BrowserThread::GetBlockingPool()->PostTask( + FROM_HERE, + base::Bind(&DeleteTempImagePath, icon_file_path_.DirName())); + } +} + +bool AppIndicatorIcon::CouldOpen() { + EnsureMethodsLoaded(); + return g_opened; +} + +void AppIndicatorIcon::SetImage(const gfx::ImageSkia& image) { + if (!g_opened) + return; + + ++icon_change_count_; + + // We create a deep copy of the image since it may have been freed by the time + // it's accessed in the other thread. + scoped_ptr safe_image(image.DeepCopy()); + base::PostTaskAndReplyWithResult( + content::BrowserThread::GetBlockingPool() + ->GetTaskRunnerWithShutdownBehavior( + base::SequencedWorkerPool::SKIP_ON_SHUTDOWN).get(), + FROM_HERE, + base::Bind(&CreateTempImageFile, + safe_image.release(), + icon_change_count_, + id_), + base::Bind(&AppIndicatorIcon::SetImageFromFile, + weak_factory_.GetWeakPtr())); +} + +void AppIndicatorIcon::SetPressedImage(const gfx::ImageSkia& image) { + // Ignore pressed images, since the standard on Linux is to not highlight + // pressed status icons. +} + +void AppIndicatorIcon::SetToolTip(const std::string& tool_tip) { + // App indicator doesn't have tooltips: + // https://bugs.launchpad.net/indicator-application/+bug/527458 +} + +void AppIndicatorIcon::SetContextMenu(ui::SimpleMenuModel* menu_model) { + menu_.reset(new MenuGtk(NULL, menu_model)); + app_indicator_set_menu(icon_, GTK_MENU(menu_->widget())); +} + +void AppIndicatorIcon::SetImageFromFile(const base::FilePath& icon_file_path) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + if (icon_file_path.empty()) + return; + + base::FilePath old_path = icon_file_path_; + icon_file_path_ = icon_file_path; + + std::string icon_name = + icon_file_path_.BaseName().RemoveExtension().value(); + std::string icon_dir = icon_file_path_.DirName().value(); + if (!icon_) { + icon_ = + app_indicator_new_with_path(id_.c_str(), + icon_name.c_str(), + APP_INDICATOR_CATEGORY_APPLICATION_STATUS, + icon_dir.c_str()); + app_indicator_set_status(icon_, APP_INDICATOR_STATUS_ACTIVE); + } else { + // Currently we are creating a new temp directory every time the icon is + // set. So we need to set the directory each time. + app_indicator_set_icon_theme_path(icon_, icon_dir.c_str()); + app_indicator_set_icon_full(icon_, icon_name.c_str(), "icon"); + + // Delete previous icon directory. + content::BrowserThread::GetBlockingPool()->PostTask( + FROM_HERE, + base::Bind(&DeleteTempImagePath, old_path.DirName())); + } +} + +} // namespace atom diff --git a/atom/browser/ui/gtk/app_indicator_icon.h b/atom/browser/ui/gtk/app_indicator_icon.h new file mode 100644 index 0000000000..1799dc1b38 --- /dev/null +++ b/atom/browser/ui/gtk/app_indicator_icon.h @@ -0,0 +1,55 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_GTK_APP_INDICATOR_ICON_H_ +#define ATOM_BROWSER_UI_GTK_APP_INDICATOR_ICON_H_ + +#include + +#include "atom/browser/ui/tray_icon.h" +#include "base/files/file_path.h" +#include "base/memory/weak_ptr.h" +#include "ui/base/gtk/gtk_signal.h" + +typedef struct _AppIndicator AppIndicator; +typedef struct _GtkWidget GtkWidget; + +class MenuGtk; + +namespace atom { + +class AppIndicatorIcon : public TrayIcon { + public: + AppIndicatorIcon(); + virtual ~AppIndicatorIcon(); + + // Indicates whether libappindicator so could be opened. + static bool CouldOpen(); + + virtual void SetImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetPressedImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetToolTip(const std::string& tool_tip) OVERRIDE; + virtual void SetContextMenu(ui::SimpleMenuModel* menu_model) OVERRIDE; + + private: + void SetImageFromFile(const base::FilePath& icon_file_path); + + // Gtk status icon wrapper + AppIndicator* icon_; + + // The context menu for this icon (if any). + scoped_ptr menu_; + + std::string id_; + base::FilePath icon_file_path_; + int icon_change_count_; + + base::WeakPtrFactory weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(AppIndicatorIcon); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_GTK_APP_INDICATOR_ICON_H_ diff --git a/atom/browser/ui/gtk/status_icon.cc b/atom/browser/ui/gtk/status_icon.cc new file mode 100644 index 0000000000..433e4af13e --- /dev/null +++ b/atom/browser/ui/gtk/status_icon.cc @@ -0,0 +1,56 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/gtk/status_icon.h" + +#include "chrome/browser/ui/gtk/menu_gtk.h" +#include "ui/gfx/gtk_util.h" + +namespace atom { + +StatusIcon::StatusIcon() : icon_(gtk_status_icon_new()) { + gtk_status_icon_set_visible(icon_, TRUE); + + g_signal_connect(icon_, "activate", G_CALLBACK(OnActivateThunk), this); + g_signal_connect(icon_, "popup-menu", G_CALLBACK(OnPopupMenuThunk), this); +} + +StatusIcon::~StatusIcon() { + gtk_status_icon_set_visible(icon_, FALSE); + g_object_unref(icon_); +} + +void StatusIcon::SetImage(const gfx::ImageSkia& image) { + if (image.isNull()) + return; + + GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(*image.bitmap()); + gtk_status_icon_set_from_pixbuf(icon_, pixbuf); + g_object_unref(pixbuf); +} + +void StatusIcon::SetPressedImage(const gfx::ImageSkia& image) { + // Ignore pressed images, since the standard on Linux is to not highlight + // pressed status icons. +} + +void StatusIcon::SetToolTip(const std::string& tool_tip) { + gtk_status_icon_set_tooltip_text(icon_, tool_tip.c_str()); +} + +void StatusIcon::SetContextMenu(ui::SimpleMenuModel* menu_model) { + menu_.reset(new MenuGtk(NULL, menu_model)); +} + +void StatusIcon::OnPopupMenu(GtkWidget* widget, guint button, guint time) { + // If we have a menu - display it. + if (menu_.get()) + menu_->PopupAsContextForStatusIcon(time, button, icon_); +} + +void StatusIcon::OnActivate(GtkWidget* widget) { + NotifyClicked(); +} + +} // namespace atom diff --git a/atom/browser/ui/gtk/status_icon.h b/atom/browser/ui/gtk/status_icon.h new file mode 100644 index 0000000000..9c8aa1f5ab --- /dev/null +++ b/atom/browser/ui/gtk/status_icon.h @@ -0,0 +1,47 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_GTK_STATUS_ICON_H_ +#define ATOM_BROWSER_UI_GTK_STATUS_ICON_H_ + +#include + +#include + +#include "atom/browser/ui/tray_icon.h" +#include "ui/base/gtk/gtk_signal.h" + +class MenuGtk; + +namespace atom { + +class StatusIcon : public TrayIcon { + public: + StatusIcon(); + virtual ~StatusIcon(); + + virtual void SetImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetPressedImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetToolTip(const std::string& tool_tip) OVERRIDE; + virtual void SetContextMenu(ui::SimpleMenuModel* menu_model) OVERRIDE; + + private: + // Callback invoked when user right-clicks on the status icon. + CHROMEGTK_CALLBACK_2(StatusIcon, void, OnPopupMenu, guint, guint); + + // Callback invoked when the icon is clicked. + CHROMEGTK_CALLBACK_0(StatusIcon, void, OnActivate); + + // The currently-displayed icon for the window. + GtkStatusIcon* icon_; + + // The context menu for this icon (if any). + scoped_ptr menu_; + + DISALLOW_COPY_AND_ASSIGN(StatusIcon); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_GTK_STATUS_ICON_H_ diff --git a/atom/browser/ui/tray_icon.cc b/atom/browser/ui/tray_icon.cc new file mode 100644 index 0000000000..fdbdf7a539 --- /dev/null +++ b/atom/browser/ui/tray_icon.cc @@ -0,0 +1,19 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/tray_icon.h" + +namespace atom { + +TrayIcon::TrayIcon() { +} + +TrayIcon::~TrayIcon() { +} + +void TrayIcon::NotifyClicked() { + FOR_EACH_OBSERVER(TrayIconObserver, observers_, OnClicked()); +} + +} // namespace atom diff --git a/atom/browser/ui/tray_icon.h b/atom/browser/ui/tray_icon.h new file mode 100644 index 0000000000..80de248810 --- /dev/null +++ b/atom/browser/ui/tray_icon.h @@ -0,0 +1,52 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_TRAY_ICON_H_ +#define ATOM_BROWSER_UI_TRAY_ICON_H_ + +#include + +#include "atom/browser/ui/tray_icon_observer.h" +#include "base/observer_list.h" +#include "ui/base/models/simple_menu_model.h" + +namespace atom { + +class TrayIcon { + public: + static TrayIcon* Create(); + + virtual ~TrayIcon(); + + // Sets the image associated with this status icon. + virtual void SetImage(const gfx::ImageSkia& image) = 0; + + // Sets the image associated with this status icon when pressed. + virtual void SetPressedImage(const gfx::ImageSkia& image) = 0; + + // Sets the hover text for this status icon. This is also used as the label + // for the menu item which is created as a replacement for the status icon + // click action on platforms that do not support custom click actions for the + // status icon (e.g. Ubuntu Unity). + virtual void SetToolTip(const std::string& tool_tip) = 0; + + // Set the context menu for this icon. + virtual void SetContextMenu(ui::SimpleMenuModel* menu_model) = 0; + + void AddObserver(TrayIconObserver* obs) { observers_.AddObserver(obs); } + void RemoveObserver(TrayIconObserver* obs) { observers_.RemoveObserver(obs); } + void NotifyClicked(); + + protected: + TrayIcon(); + + private: + ObserverList observers_; + + DISALLOW_COPY_AND_ASSIGN(TrayIcon); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_TRAY_ICON_H_ diff --git a/atom/browser/ui/tray_icon_cocoa.h b/atom/browser/ui/tray_icon_cocoa.h new file mode 100644 index 0000000000..b6a710ce12 --- /dev/null +++ b/atom/browser/ui/tray_icon_cocoa.h @@ -0,0 +1,43 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_TRAY_ICON_COCOA_H_ +#define ATOM_BROWSER_UI_TRAY_ICON_COCOA_H_ + +#import + +#include + +#include "atom/browser/ui/tray_icon.h" +#include "base/mac/scoped_nsobject.h" + +@class AtomMenuController; +@class StatusItemController; + +namespace atom { + +class TrayIconCocoa : public TrayIcon { + public: + TrayIconCocoa(); + virtual ~TrayIconCocoa(); + + virtual void SetImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetPressedImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetToolTip(const std::string& tool_tip) OVERRIDE; + virtual void SetContextMenu(ui::SimpleMenuModel* menu_model) OVERRIDE; + + private: + base::scoped_nsobject item_; + + base::scoped_nsobject controller_; + + // Status menu shown when right-clicking the system icon. + base::scoped_nsobject menu_; + + DISALLOW_COPY_AND_ASSIGN(TrayIconCocoa); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_TRAY_ICON_COCOA_H_ diff --git a/atom/browser/ui/tray_icon_cocoa.mm b/atom/browser/ui/tray_icon_cocoa.mm new file mode 100644 index 0000000000..0346163437 --- /dev/null +++ b/atom/browser/ui/tray_icon_cocoa.mm @@ -0,0 +1,81 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/tray_icon_cocoa.h" + +#include "atom/browser/ui/cocoa/atom_menu_controller.h" +#include "base/strings/sys_string_conversions.h" +#include "skia/ext/skia_utils_mac.h" + +@interface StatusItemController : NSObject { + atom::TrayIconCocoa* trayIcon_; // weak +} +- (id)initWithIcon:(atom::TrayIconCocoa*)icon; +- (void)handleClick:(id)sender; + +@end // @interface StatusItemController + +@implementation StatusItemController + +- (id)initWithIcon:(atom::TrayIconCocoa*)icon { + trayIcon_ = icon; + return self; +} + +- (void)handleClick:(id)sender { + DCHECK(trayIcon_); + trayIcon_->NotifyClicked(); +} + +@end + +namespace atom { + +TrayIconCocoa::TrayIconCocoa() { + controller_.reset([[StatusItemController alloc] initWithIcon:this]); + + item_.reset([[[NSStatusBar systemStatusBar] + statusItemWithLength:NSVariableStatusItemLength] retain]); + [item_ setEnabled:YES]; + [item_ setTarget:controller_]; + [item_ setAction:@selector(handleClick:)]; + [item_ setHighlightMode:YES]; +} + +TrayIconCocoa::~TrayIconCocoa() { + // Remove the status item from the status bar. + [[NSStatusBar systemStatusBar] removeStatusItem:item_]; +} + +void TrayIconCocoa::SetImage(const gfx::ImageSkia& image) { + if (!image.isNull()) { + NSImage* ns_image = gfx::SkBitmapToNSImage(*image.bitmap()); + if (ns_image) + [item_ setImage:ns_image]; + } +} + +void TrayIconCocoa::SetPressedImage(const gfx::ImageSkia& image) { + if (!image.isNull()) { + NSImage* ns_image = gfx::SkBitmapToNSImage(*image.bitmap()); + if (ns_image) + [item_ setAlternateImage:ns_image]; + } +} + +void TrayIconCocoa::SetToolTip(const std::string& tool_tip) { + [item_ setToolTip:base::SysUTF8ToNSString(tool_tip)]; +} + +void TrayIconCocoa::SetContextMenu(ui::SimpleMenuModel* menu_model) { + menu_.reset([[AtomMenuController alloc] initWithModel:menu_model]); + [item_ setMenu:[menu_ menu]]; +} + +// static +TrayIcon* TrayIcon::Create() { + return new TrayIconCocoa; +} + +} // namespace atom diff --git a/atom/browser/ui/tray_icon_gtk.cc b/atom/browser/ui/tray_icon_gtk.cc new file mode 100644 index 0000000000..fa9e82859c --- /dev/null +++ b/atom/browser/ui/tray_icon_gtk.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/gtk/status_icon.h" +#include "atom/browser/ui/gtk/app_indicator_icon.h" + +namespace atom { + +// static +TrayIcon* TrayIcon::Create() { + if (AppIndicatorIcon::CouldOpen()) + return new AppIndicatorIcon; + else + return new StatusIcon; +} + +} // namespace atom diff --git a/atom/browser/ui/tray_icon_observer.h b/atom/browser/ui/tray_icon_observer.h new file mode 100644 index 0000000000..f28bfd6cc9 --- /dev/null +++ b/atom/browser/ui/tray_icon_observer.h @@ -0,0 +1,20 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_TRAY_ICON_OBSERVER_H_ +#define ATOM_BROWSER_UI_TRAY_ICON_OBSERVER_H_ + +namespace atom { + +class TrayIconObserver { + public: + virtual void OnClicked() {} + + protected: + virtual ~TrayIconObserver() {} +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_TRAY_ICON_OBSERVER_H_ diff --git a/atom/browser/ui/tray_icon_win.cc b/atom/browser/ui/tray_icon_win.cc new file mode 100644 index 0000000000..f3b7581c2d --- /dev/null +++ b/atom/browser/ui/tray_icon_win.cc @@ -0,0 +1,16 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/win/notify_icon.h" +#include "atom/browser/ui/win/notify_icon_host.h" + +namespace atom { + +// static +TrayIcon* TrayIcon::Create() { + static NotifyIconHost host; + return host.CreateNotifyIcon(); +} + +} // namespace atom diff --git a/atom/browser/ui/tray_icon_win.h b/atom/browser/ui/tray_icon_win.h new file mode 100644 index 0000000000..dcff1c4807 --- /dev/null +++ b/atom/browser/ui/tray_icon_win.h @@ -0,0 +1,30 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_TRAY_ICON_WIN_H_ +#define ATOM_BROWSER_UI_TRAY_ICON_WIN_H_ + +#include + +#include "atom/browser/ui/tray_icon.h" + +namespace atom { + +class TrayIconWin : public TrayIcon { + public: + TrayIconWin(); + virtual ~TrayIconWin(); + + virtual void SetImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetPressedImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetToolTip(const std::string& tool_tip) OVERRIDE; + virtual void SetContextMenu(ui::SimpleMenuModel* menu_model) OVERRIDE; + + private: + DISALLOW_COPY_AND_ASSIGN(TrayIconWin); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_TRAY_ICON_WIN_H_ diff --git a/atom/browser/ui/win/notify_icon.cc b/atom/browser/ui/win/notify_icon.cc new file mode 100644 index 0000000000..fddee1cea4 --- /dev/null +++ b/atom/browser/ui/win/notify_icon.cc @@ -0,0 +1,133 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/win/notify_icon.h" + +#include "atom/browser/ui/win/notify_icon_host.h" +#include "atom/browser/ui/win/menu_2.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "base/win/windows_version.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/gfx/icon_util.h" +#include "ui/gfx/point.h" +#include "ui/gfx/rect.h" + +namespace atom { + +NotifyIcon::NotifyIcon(NotifyIconHost* host, + UINT id, + HWND window, + UINT message) + : host_(host), + icon_id_(id), + window_(window), + message_id_(message), + menu_model_(NULL) { + NOTIFYICONDATA icon_data; + InitIconData(&icon_data); + icon_data.uFlags = NIF_MESSAGE; + icon_data.uCallbackMessage = message_id_; + BOOL result = Shell_NotifyIcon(NIM_ADD, &icon_data); + // This can happen if the explorer process isn't running when we try to + // create the icon for some reason (for example, at startup). + if (!result) + LOG(WARNING) << "Unable to create status tray icon."; +} + +NotifyIcon::~NotifyIcon() { + // Remove our icon. + host_->Remove(this); + NOTIFYICONDATA icon_data; + InitIconData(&icon_data); + Shell_NotifyIcon(NIM_DELETE, &icon_data); +} + +void NotifyIcon::HandleClickEvent(const gfx::Point& cursor_pos, + bool left_mouse_click) { + // Pass to the observer if appropriate. + if (left_mouse_click) { + NotifyClicked(); + return; + } + + if (!menu_model_) + return; + + // Set our window as the foreground window, so the context menu closes when + // we click away from it. + if (!SetForegroundWindow(window_)) + return; + + menu_.reset(new Menu2(menu_model_)); + menu_->RunContextMenuAt(cursor_pos); +} + +void NotifyIcon::ResetIcon() { + NOTIFYICONDATA icon_data; + InitIconData(&icon_data); + // Delete any previously existing icon. + Shell_NotifyIcon(NIM_DELETE, &icon_data); + InitIconData(&icon_data); + icon_data.uFlags = NIF_MESSAGE; + icon_data.uCallbackMessage = message_id_; + icon_data.hIcon = icon_.Get(); + // If we have an image, then set the NIF_ICON flag, which tells + // Shell_NotifyIcon() to set the image for the status icon it creates. + if (icon_data.hIcon) + icon_data.uFlags |= NIF_ICON; + // Re-add our icon. + BOOL result = Shell_NotifyIcon(NIM_ADD, &icon_data); + if (!result) + LOG(WARNING) << "Unable to re-create status tray icon."; +} + +void NotifyIcon::SetImage(const gfx::ImageSkia& image) { + // Create the icon. + NOTIFYICONDATA icon_data; + InitIconData(&icon_data); + icon_data.uFlags = NIF_ICON; + icon_.Set(IconUtil::CreateHICONFromSkBitmap(*image.bitmap())); + icon_data.hIcon = icon_.Get(); + BOOL result = Shell_NotifyIcon(NIM_MODIFY, &icon_data); + if (!result) + LOG(WARNING) << "Error setting status tray icon image"; + else + host_->UpdateIconVisibilityInBackground(this); +} + +void NotifyIcon::SetPressedImage(const gfx::ImageSkia& image) { + // Ignore pressed images, since the standard on Windows is to not highlight + // pressed status icons. +} + +void NotifyIcon::SetToolTip(const std::string& tool_tip) { + // Create the icon. + NOTIFYICONDATA icon_data; + InitIconData(&icon_data); + icon_data.uFlags = NIF_TIP; + wcscpy_s(icon_data.szTip, UTF8ToUTF16(tool_tip).c_str()); + BOOL result = Shell_NotifyIcon(NIM_MODIFY, &icon_data); + if (!result) + LOG(WARNING) << "Unable to set tooltip for status tray icon"; +} + +void NotifyIcon::SetContextMenu(ui::SimpleMenuModel* menu_model) { + menu_model_ = menu_model; +} + +void NotifyIcon::InitIconData(NOTIFYICONDATA* icon_data) { + if (base::win::GetVersion() >= base::win::VERSION_VISTA) { + memset(icon_data, 0, sizeof(NOTIFYICONDATA)); + icon_data->cbSize = sizeof(NOTIFYICONDATA); + } else { + memset(icon_data, 0, NOTIFYICONDATA_V3_SIZE); + icon_data->cbSize = NOTIFYICONDATA_V3_SIZE; + } + + icon_data->hWnd = window_; + icon_data->uID = icon_id_; +} + +} // namespace atom diff --git a/atom/browser/ui/win/notify_icon.h b/atom/browser/ui/win/notify_icon.h new file mode 100644 index 0000000000..51e854b802 --- /dev/null +++ b/atom/browser/ui/win/notify_icon.h @@ -0,0 +1,79 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_WIN_NOTIFY_ICON_H_ +#define ATOM_BROWSER_UI_WIN_NOTIFY_ICON_H_ + +#include +#include + +#include + +#include "atom/browser/ui/tray_icon.h" +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/win/scoped_gdi_object.h" + +namespace gfx { +class Point; +} + +namespace atom { + +class Menu2; +class NotifyIconHost; + +class NotifyIcon : public TrayIcon { + public: + // Constructor which provides this icon's unique ID and messaging window. + NotifyIcon(NotifyIconHost* host, UINT id, HWND window, UINT message); + virtual ~NotifyIcon(); + + // Handles a click event from the user - if |left_button_click| is true and + // there is a registered observer, passes the click event to the observer, + // otherwise displays the context menu if there is one. + void HandleClickEvent(const gfx::Point& cursor_pos, bool left_button_click); + + // Re-creates the status tray icon now after the taskbar has been created. + void ResetIcon(); + + UINT icon_id() const { return icon_id_; } + HWND window() const { return window_; } + UINT message_id() const { return message_id_; } + + // Overridden from TrayIcon: + virtual void SetImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetPressedImage(const gfx::ImageSkia& image) OVERRIDE; + virtual void SetToolTip(const std::string& tool_tip) OVERRIDE; + virtual void SetContextMenu(ui::SimpleMenuModel* menu_model) OVERRIDE; + + private: + void InitIconData(NOTIFYICONDATA* icon_data); + + // The tray that owns us. Weak. + NotifyIconHost* host_; + + // The unique ID corresponding to this icon. + UINT icon_id_; + + // Window used for processing messages from this icon. + HWND window_; + + // The message identifier used for status icon messages. + UINT message_id_; + + // The currently-displayed icon for the window. + base::win::ScopedHICON icon_; + + // The context menu. + ui::SimpleMenuModel* menu_model_; + scoped_ptr menu_; + + DISALLOW_COPY_AND_ASSIGN(NotifyIcon); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_WIN_NOTIFY_ICON_H_ diff --git a/atom/browser/ui/win/notify_icon_host.cc b/atom/browser/ui/win/notify_icon_host.cc new file mode 100644 index 0000000000..7f2b54113b --- /dev/null +++ b/atom/browser/ui/win/notify_icon_host.cc @@ -0,0 +1,226 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/win/notify_icon_host.h" + +#include + +#include "atom/browser/ui/win/notify_icon.h" +#include "base/bind.h" +#include "base/stl_util.h" +#include "base/threading/non_thread_safe.h" +#include "base/threading/thread.h" +#include "base/win/wrapped_window_proc.h" +#include "chrome/browser/ui/views/status_icons/status_tray_state_changer_win.h" +#include "ui/gfx/screen.h" +#include "ui/gfx/win/hwnd_util.h" + +namespace atom { + +namespace { + +const UINT kNotifyIconMessage = WM_APP + 1; + +// |kBaseIconId| is 2 to avoid conflicts with plugins that hard-code id 1. +const UINT kBaseIconId = 2; + +const wchar_t kNotifyIconHostWindowClass[] = L"AtomShell_NotifyIconHostWindow"; + +} // namespace + +// Default implementation for NotifyIconHostStateChangerProxy that communicates +// to Exporer.exe via COM. It spawns a background thread with a fresh COM +// apartment and requests that the visibility be increased unless the user +// has explicitly set the icon to be hidden. +class NotifyIconHostStateChangerProxyImpl + : public NotifyIconHostStateChangerProxy, + public base::NonThreadSafe { + public: + NotifyIconHostStateChangerProxyImpl() + : pending_requests_(0), + worker_thread_("NotifyIconCOMWorkerThread"), + weak_factory_(this) { + worker_thread_.init_com_with_mta(false); + } + + virtual void EnqueueChange(UINT icon_id, HWND window) OVERRIDE { + DCHECK(CalledOnValidThread()); + if (pending_requests_ == 0) + worker_thread_.Start(); + + ++pending_requests_; + worker_thread_.message_loop_proxy()->PostTaskAndReply( + FROM_HERE, + base::Bind( + &NotifyIconHostStateChangerProxyImpl::EnqueueChangeOnWorkerThread, + icon_id, + window), + base::Bind(&NotifyIconHostStateChangerProxyImpl::ChangeDone, + weak_factory_.GetWeakPtr())); + } + + private: + // Must be called only on |worker_thread_|, to ensure the correct COM + // apartment. + static void EnqueueChangeOnWorkerThread(UINT icon_id, HWND window) { + // It appears that IUnknowns are coincidentally compatible with + // scoped_refptr. Normally I wouldn't depend on that but it seems that + // base::win::IUnknownImpl itself depends on that coincidence so it's + // already being assumed elsewhere. + scoped_refptr status_tray_state_changer( + new StatusTrayStateChangerWin(icon_id, window)); + status_tray_state_changer->EnsureTrayIconVisible(); + } + + // Called on UI thread. + void ChangeDone() { + DCHECK(CalledOnValidThread()); + DCHECK_GT(pending_requests_, 0); + + if (--pending_requests_ == 0) + worker_thread_.Stop(); + } + + private: + int pending_requests_; + base::Thread worker_thread_; + base::WeakPtrFactory weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(NotifyIconHostStateChangerProxyImpl); +}; + + +NotifyIconHost::NotifyIconHost() + : next_icon_id_(1), + atom_(0), + instance_(NULL), + window_(NULL) { + // Register our window class + WNDCLASSEX window_class; + base::win::InitializeWindowClass( + kNotifyIconHostWindowClass, + &base::win::WrappedWindowProc, + 0, 0, 0, NULL, NULL, NULL, NULL, NULL, + &window_class); + instance_ = window_class.hInstance; + atom_ = RegisterClassEx(&window_class); + CHECK(atom_); + + // If the taskbar is re-created after we start up, we have to rebuild all of + // our icons. + taskbar_created_message_ = RegisterWindowMessage(TEXT("TaskbarCreated")); + + // Create an offscreen window for handling messages for the status icons. We + // create a hidden WS_POPUP window instead of an HWND_MESSAGE window, because + // only top-level windows such as popups can receive broadcast messages like + // "TaskbarCreated". + window_ = CreateWindow(MAKEINTATOM(atom_), + 0, WS_POPUP, 0, 0, 0, 0, 0, 0, instance_, 0); + gfx::CheckWindowCreated(window_); + gfx::SetWindowUserData(window_, this); +} + +NotifyIconHost::~NotifyIconHost() { + if (window_) + DestroyWindow(window_); + + if (atom_) + UnregisterClass(MAKEINTATOM(atom_), instance_); + + NotifyIcons copied_container(notify_icons_); + STLDeleteContainerPointers(copied_container.begin(), copied_container.end()); +} + +NotifyIcon* NotifyIconHost::CreateNotifyIcon() { + NotifyIcon* notify_icon = + new NotifyIcon(this, NextIconId(), window_, kNotifyIconMessage); + notify_icons_.push_back(notify_icon); + return notify_icon; +} + +void NotifyIconHost::Remove(NotifyIcon* icon) { + NotifyIcons::iterator i( + std::find(notify_icons_.begin(), notify_icons_.end(), icon)); + + if (i == notify_icons_.end()) { + NOTREACHED(); + return; + } + + notify_icons_.erase(i); +} + +void NotifyIconHost::UpdateIconVisibilityInBackground( + NotifyIcon* notify_icon) { + if (!state_changer_proxy_.get()) + state_changer_proxy_.reset(new NotifyIconHostStateChangerProxyImpl); + + state_changer_proxy_->EnqueueChange(notify_icon->icon_id(), + notify_icon->window()); +} + +LRESULT CALLBACK NotifyIconHost::WndProcStatic(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + NotifyIconHost* msg_wnd = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (msg_wnd) + return msg_wnd->WndProc(hwnd, message, wparam, lparam); + else + return ::DefWindowProc(hwnd, message, wparam, lparam); +} + +LRESULT CALLBACK NotifyIconHost::WndProc(HWND hwnd, + UINT message, + WPARAM wparam, + LPARAM lparam) { + if (message == taskbar_created_message_) { + // We need to reset all of our icons because the taskbar went away. + for (NotifyIcons::const_iterator i(notify_icons_.begin()); + i != notify_icons_.end(); ++i) { + NotifyIcon* win_icon = static_cast(*i); + win_icon->ResetIcon(); + } + return TRUE; + } else if (message == kNotifyIconMessage) { + NotifyIcon* win_icon = NULL; + + // Find the selected status icon. + for (NotifyIcons::const_iterator i(notify_icons_.begin()); + i != notify_icons_.end(); ++i) { + NotifyIcon* current_win_icon = static_cast(*i); + if (current_win_icon->icon_id() == wparam) { + win_icon = current_win_icon; + break; + } + } + + // It is possible for this procedure to be called with an obsolete icon + // id. In that case we should just return early before handling any + // actions. + if (!win_icon) + return TRUE; + + switch (lparam) { + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_CONTEXTMENU: + // Walk our icons, find which one was clicked on, and invoke its + // HandleClickEvent() method. + gfx::Point cursor_pos( + gfx::Screen::GetNativeScreen()->GetCursorScreenPoint()); + win_icon->HandleClickEvent(cursor_pos, lparam == WM_LBUTTONDOWN); + return TRUE; + } + } + return ::DefWindowProc(hwnd, message, wparam, lparam); +} + +UINT NotifyIconHost::NextIconId() { + UINT icon_id = next_icon_id_++; + return kBaseIconId + icon_id; +} + +} // namespace atom diff --git a/atom/browser/ui/win/notify_icon_host.h b/atom/browser/ui/win/notify_icon_host.h new file mode 100644 index 0000000000..c4757ffd97 --- /dev/null +++ b/atom/browser/ui/win/notify_icon_host.h @@ -0,0 +1,79 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_WIN_NOTIFY_ICON_HOST_H_ +#define ATOM_BROWSER_UI_WIN_NOTIFY_ICON_HOST_H_ + +#include + +#include + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" + +namespace atom { + +class NotifyIcon; + +// A class that's responsible for increasing, if possible, the visibility +// of a status tray icon on the taskbar. The default implementation sends +// a task to a worker thread each time EnqueueChange is called. +class NotifyIconHostStateChangerProxy { + public: + // Called by NotifyIconHost to request upgraded visibility on the icon + // represented by the |icon_id|, |window| pair. + virtual void EnqueueChange(UINT icon_id, HWND window) = 0; +}; + +class NotifyIconHost { + public: + NotifyIconHost(); + ~NotifyIconHost(); + + NotifyIcon* CreateNotifyIcon(); + void Remove(NotifyIcon* notify_icon); + + void UpdateIconVisibilityInBackground(NotifyIcon* notify_icon); + + private: + typedef std::vector NotifyIcons; + + // Static callback invoked when a message comes in to our messaging window. + static LRESULT CALLBACK + WndProcStatic(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + LRESULT CALLBACK + WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + UINT NextIconId(); + + // The unique icon ID we will assign to the next icon. + UINT next_icon_id_; + + // List containing all active NotifyIcons. + NotifyIcons notify_icons_; + + // The window class of |window_|. + ATOM atom_; + + // The handle of the module that contains the window procedure of |window_|. + HMODULE instance_; + + // The window used for processing events. + HWND window_; + + // The message ID of the "TaskbarCreated" message, sent to us when we need to + // reset our status icons. + UINT taskbar_created_message_; + + // Manages changes performed on a background thread to manipulate visibility + // of notification icons. + scoped_ptr state_changer_proxy_; + + DISALLOW_COPY_AND_ASSIGN(NotifyIconHost); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_WIN_NOTIFY_ICON_HOST_H_ diff --git a/atom/common/api/atom_extensions.h b/atom/common/api/atom_extensions.h index 6d0a9166c1..39a4159c55 100644 --- a/atom/common/api/atom_extensions.h +++ b/atom/common/api/atom_extensions.h @@ -15,6 +15,7 @@ NODE_EXT_LIST_ITEM(atom_browser_dialog) NODE_EXT_LIST_ITEM(atom_browser_menu) NODE_EXT_LIST_ITEM(atom_browser_power_monitor) NODE_EXT_LIST_ITEM(atom_browser_protocol) +NODE_EXT_LIST_ITEM(atom_browser_tray) NODE_EXT_LIST_ITEM(atom_browser_window) // Module names start with `atom_renderer_` can only be used by renderer diff --git a/atom/common/native_mate_converters/image_converter.cc b/atom/common/native_mate_converters/image_converter.cc new file mode 100644 index 0000000000..5564f1e5df --- /dev/null +++ b/atom/common/native_mate_converters/image_converter.cc @@ -0,0 +1,45 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/common/native_mate_converters/image_converter.h" + +#include + +#include "atom/common/native_mate_converters/file_path_converter.h" +#include "base/file_util.h" +#include "ui/gfx/codec/jpeg_codec.h" +#include "ui/gfx/codec/png_codec.h" +#include "ui/gfx/image/image_skia.h" + +namespace mate { + +bool Converter::FromV8(v8::Isolate* isolate, + v8::Handle val, + gfx::ImageSkia* out) { + base::FilePath path; + if (Converter::FromV8(isolate, val, &path)) { + std::string file_contents; + if (!base::ReadFileToString(path, &file_contents)) + return false; + + const unsigned char* data = + reinterpret_cast(file_contents.data()); + size_t size = file_contents.size(); + scoped_ptr decoded(new SkBitmap()); + + // Try PNG first. + if (!gfx::PNGCodec::Decode(data, size, decoded.get())) + // Try JPEG. + decoded.reset(gfx::JPEGCodec::Decode(data, size)); + + if (decoded) { + *out = gfx::ImageSkia::CreateFrom1xBitmap(*decoded.release()); + return true; + } + } + + return false; +} + +} // namespace mate diff --git a/atom/common/native_mate_converters/image_converter.h b/atom/common/native_mate_converters/image_converter.h new file mode 100644 index 0000000000..c5032a8729 --- /dev/null +++ b/atom/common/native_mate_converters/image_converter.h @@ -0,0 +1,25 @@ +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_COMMON_NATIVE_MATE_CONVERTERS_IMAGE_CONVERTER_H_ +#define ATOM_COMMON_NATIVE_MATE_CONVERTERS_IMAGE_CONVERTER_H_ + +#include "native_mate/converter.h" + +namespace gfx { +class ImageSkia; +} + +namespace mate { + +template<> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Handle val, + gfx::ImageSkia* out); +}; + +} // namespace mate + +#endif // ATOM_COMMON_NATIVE_MATE_CONVERTERS_IMAGE_CONVERTER_H_ diff --git a/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.cc b/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.cc new file mode 100644 index 0000000000..410d31db62 --- /dev/null +++ b/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.cc @@ -0,0 +1,236 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/ui/views/status_icons/status_tray_state_changer_win.h" + +namespace { + +//////////////////////////////////////////////////////////////////////////////// +// Status Tray API + +// The folowing describes the interface to the undocumented Windows Exporer APIs +// for manipulating with the status tray area. This code should be used with +// care as it can change with versions (even minor versions) of Windows. + +// ITrayNotify is an interface describing the API for manipulating the state of +// the Windows notification area, as well as for registering for change +// notifications. +class __declspec(uuid("FB852B2C-6BAD-4605-9551-F15F87830935")) ITrayNotify + : public IUnknown { + public: + virtual HRESULT STDMETHODCALLTYPE + RegisterCallback(INotificationCB* callback) = 0; + virtual HRESULT STDMETHODCALLTYPE + SetPreference(const NOTIFYITEM* notify_item) = 0; + virtual HRESULT STDMETHODCALLTYPE EnableAutoTray(BOOL enabled) = 0; +}; + +// ITrayNotifyWin8 is the interface that replaces ITrayNotify for newer versions +// of Windows. +class __declspec(uuid("D133CE13-3537-48BA-93A7-AFCD5D2053B4")) ITrayNotifyWin8 + : public IUnknown { + public: + virtual HRESULT STDMETHODCALLTYPE + RegisterCallback(INotificationCB* callback, unsigned long*) = 0; + virtual HRESULT STDMETHODCALLTYPE UnregisterCallback(unsigned long*) = 0; + virtual HRESULT STDMETHODCALLTYPE SetPreference(NOTIFYITEM const*) = 0; + virtual HRESULT STDMETHODCALLTYPE EnableAutoTray(BOOL) = 0; + virtual HRESULT STDMETHODCALLTYPE DoAction(BOOL) = 0; +}; + +const CLSID CLSID_TrayNotify = { + 0x25DEAD04, + 0x1EAC, + 0x4911, + {0x9E, 0x3A, 0xAD, 0x0A, 0x4A, 0xB5, 0x60, 0xFD}}; + +} // namespace + +StatusTrayStateChangerWin::StatusTrayStateChangerWin(UINT icon_id, HWND window) + : interface_version_(INTERFACE_VERSION_UNKNOWN), + icon_id_(icon_id), + window_(window) { + wchar_t module_name[MAX_PATH]; + ::GetModuleFileName(NULL, module_name, MAX_PATH); + + file_name_ = module_name; +} + +void StatusTrayStateChangerWin::EnsureTrayIconVisible() { + DCHECK(CalledOnValidThread()); + + if (!CreateTrayNotify()) { + VLOG(1) << "Unable to create COM object for ITrayNotify."; + return; + } + + scoped_ptr notify_item = RegisterCallback(); + + // If the user has already hidden us explicitly, try to honor their choice by + // not changing anything. + if (notify_item->preference == PREFERENCE_SHOW_NEVER) + return; + + // If we are already on the taskbar, return since nothing needs to be done. + if (notify_item->preference == PREFERENCE_SHOW_ALWAYS) + return; + + notify_item->preference = PREFERENCE_SHOW_ALWAYS; + + SendNotifyItemUpdate(notify_item.Pass()); +} + +STDMETHODIMP_(ULONG) StatusTrayStateChangerWin::AddRef() { + DCHECK(CalledOnValidThread()); + return base::win::IUnknownImpl::AddRef(); +} + +STDMETHODIMP_(ULONG) StatusTrayStateChangerWin::Release() { + DCHECK(CalledOnValidThread()); + return base::win::IUnknownImpl::Release(); +} + +STDMETHODIMP StatusTrayStateChangerWin::QueryInterface(REFIID riid, + PVOID* ptr_void) { + DCHECK(CalledOnValidThread()); + if (riid == __uuidof(INotificationCB)) { + *ptr_void = static_cast(this); + AddRef(); + return S_OK; + } + + return base::win::IUnknownImpl::QueryInterface(riid, ptr_void); +} + +STDMETHODIMP StatusTrayStateChangerWin::Notify(ULONG event, + NOTIFYITEM* notify_item) { + DCHECK(CalledOnValidThread()); + DCHECK(notify_item); + if (notify_item->hwnd != window_ || notify_item->id != icon_id_ || + base::string16(notify_item->exe_name) != file_name_) { + return S_OK; + } + + notify_item_.reset(new NOTIFYITEM(*notify_item)); + return S_OK; +} + +StatusTrayStateChangerWin::~StatusTrayStateChangerWin() { + DCHECK(CalledOnValidThread()); +} + +bool StatusTrayStateChangerWin::CreateTrayNotify() { + DCHECK(CalledOnValidThread()); + + tray_notify_.Release(); // Release so this method can be called more than + // once. + + HRESULT hr = tray_notify_.CreateInstance(CLSID_TrayNotify); + if (FAILED(hr)) + return false; + + base::win::ScopedComPtr tray_notify_win8; + hr = tray_notify_win8.QueryFrom(tray_notify_); + if (SUCCEEDED(hr)) { + interface_version_ = INTERFACE_VERSION_WIN8; + return true; + } + + base::win::ScopedComPtr tray_notify_legacy; + hr = tray_notify_legacy.QueryFrom(tray_notify_); + if (SUCCEEDED(hr)) { + interface_version_ = INTERFACE_VERSION_LEGACY; + return true; + } + + return false; +} + +scoped_ptr StatusTrayStateChangerWin::RegisterCallback() { + // |notify_item_| is used to store the result of the callback from + // Explorer.exe, which happens synchronously during + // RegisterCallbackWin8 or RegisterCallbackLegacy. + DCHECK(notify_item_.get() == NULL); + + // TODO(dewittj): Add UMA logging here to report if either of our strategies + // has a tendency to fail on particular versions of Windows. + switch (interface_version_) { + case INTERFACE_VERSION_WIN8: + if (!RegisterCallbackWin8()) + VLOG(1) << "Unable to successfully run RegisterCallbackWin8."; + break; + case INTERFACE_VERSION_LEGACY: + if (!RegisterCallbackLegacy()) + VLOG(1) << "Unable to successfully run RegisterCallbackLegacy."; + break; + default: + NOTREACHED(); + } + + // Adding an intermediate scoped pointer here so that |notify_item_| is reset + // to NULL. + scoped_ptr rv(notify_item_.release()); + return rv.Pass(); +} + +bool StatusTrayStateChangerWin::RegisterCallbackWin8() { + base::win::ScopedComPtr tray_notify_win8; + HRESULT hr = tray_notify_win8.QueryFrom(tray_notify_); + if (FAILED(hr)) + return false; + + // The following two lines cause Windows Explorer to call us back with all the + // existing tray icons and their preference. It would also presumably notify + // us if changes were made in realtime while we registered as a callback, but + // we just want to modify our own entry so we immediately unregister. + unsigned long callback_id = 0; + hr = tray_notify_win8->RegisterCallback(this, &callback_id); + tray_notify_win8->UnregisterCallback(&callback_id); + if (FAILED(hr)) { + return false; + } + + return true; +} + +bool StatusTrayStateChangerWin::RegisterCallbackLegacy() { + base::win::ScopedComPtr tray_notify; + HRESULT hr = tray_notify.QueryFrom(tray_notify_); + if (FAILED(hr)) { + return false; + } + + // The following two lines cause Windows Explorer to call us back with all the + // existing tray icons and their preference. It would also presumably notify + // us if changes were made in realtime while we registered as a callback. In + // this version of the API, there can be only one registered callback so it is + // better to unregister as soon as possible. + // TODO(dewittj): Try to notice if the notification area icon customization + // window is open and postpone this call until the user closes it; + // registering the callback while the window is open can cause stale data to + // be displayed to the user. + hr = tray_notify->RegisterCallback(this); + tray_notify->RegisterCallback(NULL); + if (FAILED(hr)) { + return false; + } + + return true; +} + +void StatusTrayStateChangerWin::SendNotifyItemUpdate( + scoped_ptr notify_item) { + if (interface_version_ == INTERFACE_VERSION_LEGACY) { + base::win::ScopedComPtr tray_notify; + HRESULT hr = tray_notify.QueryFrom(tray_notify_); + if (SUCCEEDED(hr)) + tray_notify->SetPreference(notify_item.get()); + } else if (interface_version_ == INTERFACE_VERSION_WIN8) { + base::win::ScopedComPtr tray_notify; + HRESULT hr = tray_notify.QueryFrom(tray_notify_); + if (SUCCEEDED(hr)) + tray_notify->SetPreference(notify_item.get()); + } +} + diff --git a/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.h b/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.h new file mode 100644 index 0000000000..963792b980 --- /dev/null +++ b/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.h @@ -0,0 +1,133 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_UI_VIEWS_STATUS_ICONS_STATUS_TRAY_STATE_CHANGER_WIN_H_ +#define CHROME_BROWSER_UI_VIEWS_STATUS_ICONS_STATUS_TRAY_STATE_CHANGER_WIN_H_ + +#include "base/memory/scoped_ptr.h" +#include "base/strings/string16.h" +#include "base/threading/non_thread_safe.h" +#include "base/win/iunknown_impl.h" +#include "base/win/scoped_comptr.h" + +// The known values for NOTIFYITEM's dwPreference member. +enum NOTIFYITEM_PREFERENCE { + // In Windows UI: "Only show notifications." + PREFERENCE_SHOW_WHEN_ACTIVE = 0, + // In Windows UI: "Hide icon and notifications." + PREFERENCE_SHOW_NEVER = 1, + // In Windows UI: "Show icon and notifications." + PREFERENCE_SHOW_ALWAYS = 2 +}; + +// NOTIFYITEM describes an entry in Explorer's registry of status icons. +// Explorer keeps entries around for a process even after it exits. +struct NOTIFYITEM { + PWSTR exe_name; // The file name of the creating executable. + PWSTR tip; // The last hover-text value associated with this status + // item. + HICON icon; // The icon associated with this status item. + HWND hwnd; // The HWND associated with the status item. + DWORD preference; // Determines the behavior of the icon with respect to + // the taskbar. Values taken from NOTIFYITEM_PREFERENCE. + UINT id; // The ID specified by the application. (hWnd, uID) is + // unique. + GUID guid; // The GUID specified by the application, alternative to + // uID. +}; + +// INotificationCB is an interface that applications can implement in order to +// receive notifications about the state of the notification area manager. +class __declspec(uuid("D782CCBA-AFB0-43F1-94DB-FDA3779EACCB")) INotificationCB + : public IUnknown { + public: + virtual HRESULT STDMETHODCALLTYPE + Notify(ULONG event, NOTIFYITEM* notify_item) = 0; +}; + +// A class that is capable of reading and writing the state of the notification +// area in the Windows taskbar. It is used to promote a tray icon from the +// overflow area to the taskbar, and refuses to do anything if the user has +// explicitly marked an icon to be always hidden. +class StatusTrayStateChangerWin : public INotificationCB, + public base::win::IUnknownImpl, + public base::NonThreadSafe { + public: + StatusTrayStateChangerWin(UINT icon_id, HWND window); + + // Call this method to move the icon matching |icon_id| and |window| to the + // taskbar from the overflow area. This will not make any changes if the + // icon has been set to |PREFERENCE_SHOW_NEVER|, in order to comply with + // the explicit wishes/configuration of the user. + void EnsureTrayIconVisible(); + + // IUnknown. + virtual ULONG STDMETHODCALLTYPE AddRef() OVERRIDE; + virtual ULONG STDMETHODCALLTYPE Release() OVERRIDE; + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, PVOID*) OVERRIDE; + + // INotificationCB. + // Notify is called in response to RegisterCallback for each current + // entry in Explorer's list of notification area icons, and ever time + // one of them changes, until UnregisterCallback is called or |this| + // is destroyed. + virtual HRESULT STDMETHODCALLTYPE Notify(ULONG, NOTIFYITEM*); + + protected: + virtual ~StatusTrayStateChangerWin(); + + private: + friend class StatusTrayStateChangerWinTest; + + enum InterfaceVersion { + INTERFACE_VERSION_LEGACY = 0, + INTERFACE_VERSION_WIN8, + INTERFACE_VERSION_UNKNOWN + }; + + // Creates an instance of TrayNotify, and ensures that it supports either + // ITrayNotify or ITrayNotifyWin8. Returns true on success. + bool CreateTrayNotify(); + + // Returns the NOTIFYITEM that corresponds to this executable and the + // HWND/ID pair that were used to create the StatusTrayStateChangerWin. + // Internally it calls the appropriate RegisterCallback{Win8,Legacy}. + scoped_ptr RegisterCallback(); + + // Calls RegisterCallback with the appropriate interface required by + // different versions of Windows. This will result in |notify_item_| being + // updated when a matching item is passed into + // StatusTrayStateChangerWin::Notify. + bool RegisterCallbackWin8(); + bool RegisterCallbackLegacy(); + + // Sends an update to Explorer with the passed NOTIFYITEM. + void SendNotifyItemUpdate(scoped_ptr notify_item); + + // Storing IUnknown since we will need to use different interfaces + // for different versions of Windows. + base::win::ScopedComPtr tray_notify_; + InterfaceVersion interface_version_; + + // The ID assigned to the notification area icon that we want to manipulate. + const UINT icon_id_; + // The HWND associated with the notification area icon that we want to + // manipulate. This is an unretained pointer, do not dereference. + const HWND window_; + // Executable name of the current program. Along with |icon_id_| and + // |window_|, this uniquely identifies a notification area entry to Explorer. + base::string16 file_name_; + + // Temporary storage for the matched NOTIFYITEM. This is necessary because + // Notify doesn't return anything. The call flow looks like this: + // TrayNotify->RegisterCallback() + // ... other COM stack frames .. + // StatusTrayStateChangerWin->Notify(NOTIFYITEM); + // so we can't just return the notifyitem we're looking for. + scoped_ptr notify_item_; + + DISALLOW_COPY_AND_ASSIGN(StatusTrayStateChangerWin); +}; + +#endif // CHROME_BROWSER_UI_VIEWS_STATUS_ICONS_STATUS_TRAY_STATE_CHANGER_WIN_H_ \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 82ab7c5228..69358530fb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,7 @@ Modules for browser side: * [menu-item](api/menu-item.md) * [power-monitor](api/power-monitor.md) * [protocol](api/protocol.md) +* [tray](api/tray.md) Modules for web page: diff --git a/docs/api/tray.md b/docs/api/tray.md new file mode 100644 index 0000000000..f35fc4bdcf --- /dev/null +++ b/docs/api/tray.md @@ -0,0 +1,66 @@ +# tray + +A `Tray` represents an icon in operating system's notification area, it is +usually attached with a context menu. + +```javascript +var Menu = require('menu'); +var Tray = require('tray'); + +var appIcon = new Tray('/path/to/my/icon'); +var contextMenu = Menu.buildFromTemplate([ + { label: 'Item1', type: 'radio' }, + { label: 'Item2', type: 'radio' }, + { label: 'Item3', type: 'radio', clicked: true }, + { label: 'Item4', type: 'radio' }, +]); +appIcon.setToolTip('This is my application.'); +appIcon.setContextMenu(contextMenu); +``` + +__Platform limitations:__ + +* On OS X `clicked` event will be ignored if the tray icon has context menu. +* On Linux app indicator will be used if it is supported, otherwise + `GtkStatusIcon` will be used instead. +* App indicator will only be showed when it has context menu. +* When app indicator is used on Linux, `clicked` event is ignored. + +So if you want to keep exact same behaviors on all platforms, you should not +rely on `clicked` event and always attach a context menu to the tray icon. + +## Class: Tray + +`Tray` is an [EventEmitter](event-emitter). + +### new Tray(image) + +* `image` String + +Creates a new tray icon associated with the `image`. + +### Event: 'clicked' + +Emitted when the tray icon is clicked. + +### Tray.setImage(image) + +* `image` String + +Sets the `image` associated with this tray icon. + +### Tray.setPressedImage(image) + +* `image` String + +Sets the `image` associated with this tray icon when pressed. + +### Tray.setToolTip(toolTip) + +* `toolTip` String + +### Tray.setContextMenu(menu) + +* `menu` Menu + +[event-emitter]: http://nodejs.org/api/events.html#events_class_events_eventemitter