diff --git a/atom/browser/api/atom_api_app.cc b/atom/browser/api/atom_api_app.cc index 4f926f55ba73..cf935f899303 100644 --- a/atom/browser/api/atom_api_app.cc +++ b/atom/browser/api/atom_api_app.cc @@ -44,6 +44,7 @@ #include "ui/gfx/image/image.h" #if defined(OS_WIN) +#include "atom/browser/ui/win/jump_list.h" #include "base/strings/utf_string_conversions.h" #endif @@ -70,6 +71,220 @@ struct Converter { return true; } }; + +using atom::JumpListItem; +using atom::JumpListCategory; +using atom::JumpListResult; + +template<> +struct Converter { + static bool FromV8(v8::Isolate* isolate, v8::Local val, + JumpListItem::Type* out) { + std::string item_type; + if (!ConvertFromV8(isolate, val, &item_type)) + return false; + + if (item_type == "task") + *out = JumpListItem::Type::TASK; + else if (item_type == "separator") + *out = JumpListItem::Type::SEPARATOR; + else if (item_type == "file") + *out = JumpListItem::Type::FILE; + else + return false; + + return true; + } + + static v8::Local ToV8(v8::Isolate* isolate, + JumpListItem::Type val) { + std::string item_type; + switch (val) { + case JumpListItem::Type::TASK: + item_type = "task"; + break; + + case JumpListItem::Type::SEPARATOR: + item_type = "separator"; + break; + + case JumpListItem::Type::FILE: + item_type = "file"; + break; + } + return mate::ConvertToV8(isolate, item_type); + } +}; + +template<> +struct Converter { + static bool FromV8(v8::Isolate* isolate, v8::Local val, + JumpListItem* out) { + mate::Dictionary dict; + if (!ConvertFromV8(isolate, val, &dict)) + return false; + + if (!dict.Get("type", &(out->type))) + return false; + + switch (out->type) { + case JumpListItem::Type::TASK: + if (!dict.Get("program", &(out->path)) || + !dict.Get("title", &(out->title))) + return false; + + if (dict.Get("iconPath", &(out->icon_path)) && + !dict.Get("iconIndex", &(out->icon_index))) + return false; + + dict.Get("args", &(out->arguments)); + dict.Get("description", &(out->description)); + return true; + + case JumpListItem::Type::SEPARATOR: + return true; + + case JumpListItem::Type::FILE: + return dict.Get("path", &(out->path)); + } + + assert(false); + return false; + } + + static v8::Local ToV8(v8::Isolate* isolate, + const JumpListItem& val) { + mate::Dictionary dict = mate::Dictionary::CreateEmpty(isolate); + dict.Set("type", val.type); + + switch (val.type) { + case JumpListItem::Type::TASK: + dict.Set("program", val.path); + dict.Set("args", val.arguments); + dict.Set("title", val.title); + dict.Set("iconPath", val.icon_path); + dict.Set("iconIndex", val.icon_index); + dict.Set("description", val.description); + break; + + case JumpListItem::Type::SEPARATOR: + break; + + case JumpListItem::Type::FILE: + dict.Set("path", val.path); + break; + } + return dict.GetHandle(); + } +}; + +template<> +struct Converter { + static bool FromV8(v8::Isolate* isolate, v8::Local val, + JumpListCategory::Type* out) { + std::string category_type; + if (!ConvertFromV8(isolate, val, &category_type)) + return false; + + if (category_type == "tasks") + *out = JumpListCategory::Type::TASKS; + else if (category_type == "frequent") + *out = JumpListCategory::Type::FREQUENT; + else if (category_type == "recent") + *out = JumpListCategory::Type::RECENT; + else if (category_type == "custom") + *out = JumpListCategory::Type::CUSTOM; + else + return false; + + return true; + } + + static v8::Local ToV8(v8::Isolate* isolate, + JumpListCategory::Type val) { + std::string category_type; + switch (val) { + case JumpListCategory::Type::TASKS: + category_type = "tasks"; + break; + + case JumpListCategory::Type::FREQUENT: + category_type = "frequent"; + break; + + case JumpListCategory::Type::RECENT: + category_type = "recent"; + break; + + case JumpListCategory::Type::CUSTOM: + category_type = "custom"; + break; + } + return mate::ConvertToV8(isolate, category_type); + } +}; + +template<> +struct Converter { + static bool FromV8(v8::Isolate* isolate, v8::Local val, + JumpListCategory* out) { + mate::Dictionary dict; + if (!ConvertFromV8(isolate, val, &dict)) + return false; + + if (dict.Get("name", &(out->name)) && out->name.empty()) + return false; + + if (!dict.Get("type", &(out->type))) { + if (out->name.empty()) + out->type = JumpListCategory::Type::TASKS; + else + out->type = JumpListCategory::Type::CUSTOM; + } + + if ((out->type == JumpListCategory::Type::TASKS) || + (out->type == JumpListCategory::Type::CUSTOM)) { + if (!dict.Get("items", &(out->items))) + return false; + } + + return true; + } +}; + +// static +template<> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, JumpListResult val) { + std::string result_code; + switch (val) { + case JumpListResult::SUCCESS: + result_code = "ok"; + break; + + case JumpListResult::ARGUMENT_ERROR: + result_code = "argumentError"; + break; + + case JumpListResult::GENERIC_ERROR: + result_code = "error"; + break; + + case JumpListResult::CUSTOM_CATEGORY_SEPARATOR_ERROR: + result_code = "invalidSeparatorError"; + break; + + case JumpListResult::MISSING_FILE_TYPE_REGISTRATION_ERROR: + result_code = "fileTypeRegistrationError"; + break; + + case JumpListResult::CUSTOM_CATEGORY_ACCESS_DENIED_ERROR: + result_code = "customCategoryAccessDeniedError"; + break; + } + return ConvertToV8(isolate, result_code); + } +}; #endif template<> @@ -523,6 +738,63 @@ void App::OnCertificateManagerModelCreated( } #endif +#if defined(OS_WIN) +v8::Local App::GetJumpListSettings() { + JumpList jump_list(Browser::Get()->GetAppUserModelID()); + + int min_items = 10; + std::vector removed_items; + if (jump_list.Begin(&min_items, &removed_items)) { + // We don't actually want to change anything, so abort the transaction. + jump_list.Abort(); + } else { + LOG(ERROR) << "Failed to begin Jump List transaction."; + } + + auto dict = mate::Dictionary::CreateEmpty(isolate()); + dict.Set("minItems", min_items); + dict.Set("removedItems", mate::ConvertToV8(isolate(), removed_items)); + return dict.GetHandle(); +} + +JumpListResult App::SetJumpList(v8::Local val, + mate::Arguments* args) { + std::vector categories; + bool delete_jump_list = val->IsNull(); + if (!delete_jump_list && + !mate::ConvertFromV8(args->isolate(), val, &categories)) { + args->ThrowError("Argument must be null or an array of categories"); + return JumpListResult::ARGUMENT_ERROR; + } + + JumpList jump_list(Browser::Get()->GetAppUserModelID()); + + if (delete_jump_list) { + return jump_list.Delete() + ? JumpListResult::SUCCESS + : JumpListResult::GENERIC_ERROR; + } + + // Start a transaction that updates the JumpList of this application. + if (!jump_list.Begin()) + return JumpListResult::GENERIC_ERROR; + + JumpListResult result = jump_list.AppendCategories(categories); + // AppendCategories may have failed to add some categories, but it's better + // to have something than nothing so try to commit the changes anyway. + if (!jump_list.Commit()) { + LOG(ERROR) << "Failed to commit changes to custom Jump List."; + // It's more useful to return the earlier error code that might give + // some indication as to why the transaction actually failed, so don't + // overwrite it with a "generic error" code here. + if (result == JumpListResult::SUCCESS) + result = JumpListResult::GENERIC_ERROR; + } + + return result; +} +#endif // defined(OS_WIN) + // static mate::Handle App::Create(v8::Isolate* isolate) { return mate::CreateHandle(isolate, new App(isolate)); @@ -570,6 +842,8 @@ void App::BuildPrototype( #endif #if defined(OS_WIN) .SetMethod("setUserTasks", base::Bind(&Browser::SetUserTasks, browser)) + .SetMethod("getJumpListSettings", &App::GetJumpListSettings) + .SetMethod("setJumpList", &App::SetJumpList) #endif #if defined(OS_LINUX) .SetMethod("isUnityRunning", diff --git a/atom/browser/api/atom_api_app.h b/atom/browser/api/atom_api_app.h index 990199cd6fe5..187aba184abd 100644 --- a/atom/browser/api/atom_api_app.h +++ b/atom/browser/api/atom_api_app.h @@ -26,10 +26,14 @@ class FilePath; namespace mate { class Arguments; -} +} // namespace mate namespace atom { +#if defined(OS_WIN) +enum class JumpListResult : int; +#endif + namespace api { class App : public AtomBrowserClient::Delegate, @@ -120,6 +124,14 @@ class App : public AtomBrowserClient::Delegate, const net::CompletionCallback& callback); #endif +#if defined(OS_WIN) + // Get the current Jump List settings. + v8::Local GetJumpListSettings(); + + // Set or remove a custom Jump List for the application. + JumpListResult SetJumpList(v8::Local val, mate::Arguments* args); +#endif // defined(OS_WIN) + std::unique_ptr process_singleton_; #if defined(USE_NSS_CERTS) diff --git a/atom/browser/browser_win.cc b/atom/browser/browser_win.cc index ba33e55751e7..f9dfdc207a03 100644 --- a/atom/browser/browser_win.cc +++ b/atom/browser/browser_win.cc @@ -7,10 +7,10 @@ #include // windows.h must be included first #include -#include #include #include +#include "atom/browser/ui/win/jump_list.h" #include "atom/common/atom_version.h" #include "atom/common/native_mate_converters/string16_converter.h" #include "base/base_paths.h" @@ -104,49 +104,27 @@ void Browser::SetAppUserModelID(const base::string16& name) { } bool Browser::SetUserTasks(const std::vector& tasks) { - CComPtr destinations; - if (FAILED(destinations.CoCreateInstance(CLSID_DestinationList))) - return false; - if (FAILED(destinations->SetAppID(GetAppUserModelID()))) + JumpList jump_list(GetAppUserModelID()); + if (!jump_list.Begin()) return false; - // Start a transaction that updates the JumpList of this application. - UINT max_slots; - CComPtr removed; - if (FAILED(destinations->BeginList(&max_slots, IID_PPV_ARGS(&removed)))) - return false; - - CComPtr collection; - if (FAILED(collection.CoCreateInstance(CLSID_EnumerableObjectCollection))) - return false; - - for (auto& task : tasks) { - CComPtr link; - if (FAILED(link.CoCreateInstance(CLSID_ShellLink)) || - FAILED(link->SetPath(task.program.value().c_str())) || - FAILED(link->SetArguments(task.arguments.c_str())) || - FAILED(link->SetDescription(task.description.c_str()))) - return false; - - if (!task.icon_path.empty() && - FAILED(link->SetIconLocation(task.icon_path.value().c_str(), - task.icon_index))) - return false; - - CComQIPtr property_store = link; - if (!base::win::SetStringValueForPropertyStore(property_store, PKEY_Title, - task.title.c_str())) - return false; - - if (FAILED(collection->AddObject(link))) - return false; + JumpListCategory category; + category.type = JumpListCategory::Type::TASKS; + category.items.reserve(tasks.size()); + JumpListItem item; + item.type = JumpListItem::Type::TASK; + for (const auto& task : tasks) { + item.title = task.title; + item.path = task.program; + item.arguments = task.arguments; + item.icon_path = task.icon_path; + item.icon_index = task.icon_index; + item.description = task.description; + category.items.push_back(item); } - // When the list is empty "AddUserTasks" could fail, so we don't check return - // value for it. - CComQIPtr task_array = collection; - destinations->AddUserTasks(task_array); - return SUCCEEDED(destinations->CommitList()); + jump_list.AppendCategory(category); + return jump_list.Commit(); } bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol, diff --git a/atom/browser/ui/win/jump_list.cc b/atom/browser/ui/win/jump_list.cc new file mode 100644 index 000000000000..bf6f3139786b --- /dev/null +++ b/atom/browser/ui/win/jump_list.cc @@ -0,0 +1,332 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/win/jump_list.h" + +#include // for PKEY_* constants + +#include "base/win/scoped_co_mem.h" +#include "base/win/scoped_propvariant.h" +#include "base/win/win_util.h" + +namespace { + +using atom::JumpListItem; +using atom::JumpListCategory; +using atom::JumpListResult; + +bool AppendTask(const JumpListItem& item, IObjectCollection* collection) { + DCHECK(collection); + + CComPtr link; + if (FAILED(link.CoCreateInstance(CLSID_ShellLink)) || + FAILED(link->SetPath(item.path.value().c_str())) || + FAILED(link->SetArguments(item.arguments.c_str())) || + FAILED(link->SetDescription(item.description.c_str()))) + return false; + + if (!item.icon_path.empty() && + FAILED(link->SetIconLocation(item.icon_path.value().c_str(), + item.icon_index))) + return false; + + CComQIPtr property_store = link; + if (!base::win::SetStringValueForPropertyStore(property_store, PKEY_Title, + item.title.c_str())) + return false; + + return SUCCEEDED(collection->AddObject(link)); +} + +bool AppendSeparator(IObjectCollection* collection) { + DCHECK(collection); + + CComPtr shell_link; + if (SUCCEEDED(shell_link.CoCreateInstance(CLSID_ShellLink))) { + CComQIPtr property_store = shell_link; + if (base::win::SetBooleanValueForPropertyStore( + property_store, PKEY_AppUserModel_IsDestListSeparator, true)) + return SUCCEEDED(collection->AddObject(shell_link)); + } + return false; +} + +bool AppendFile(const JumpListItem& item, IObjectCollection* collection) { + DCHECK(collection); + + CComPtr file; + if (SUCCEEDED(SHCreateItemFromParsingName( + item.path.value().c_str(), NULL, IID_PPV_ARGS(&file)))) + return SUCCEEDED(collection->AddObject(file)); + + return false; +} + +bool GetShellItemFileName(IShellItem* shell_item, base::FilePath* file_name) { + DCHECK(shell_item); + DCHECK(file_name); + + base::win::ScopedCoMem file_name_buffer; + if (SUCCEEDED(shell_item->GetDisplayName(SIGDN_FILESYSPATH, + &file_name_buffer))) { + *file_name = base::FilePath(file_name_buffer.get()); + return true; + } + return false; +} + +bool ConvertShellLinkToJumpListItem(IShellLink* shell_link, + JumpListItem* item) { + DCHECK(shell_link); + DCHECK(item); + + item->type = JumpListItem::Type::TASK; + wchar_t path[MAX_PATH]; + if (FAILED(shell_link->GetPath(path, arraysize(path), nullptr, 0))) + return false; + + CComQIPtr property_store = shell_link; + base::win::ScopedPropVariant prop; + if (SUCCEEDED(property_store->GetValue(PKEY_Link_Arguments, prop.Receive())) + && (prop.get().vt == VT_LPWSTR)) { + item->arguments = prop.get().pwszVal; + } + + if (SUCCEEDED(property_store->GetValue(PKEY_Title, prop.Receive())) + && (prop.get().vt == VT_LPWSTR)) { + item->title = prop.get().pwszVal; + } + + int icon_index; + if (SUCCEEDED(shell_link->GetIconLocation(path, arraysize(path), + &icon_index))) { + item->icon_path = base::FilePath(path); + item->icon_index = icon_index; + } + + wchar_t item_desc[INFOTIPSIZE]; + if (SUCCEEDED(shell_link->GetDescription(item_desc, arraysize(item_desc)))) + item->description = item_desc; + + return true; +} + +// Convert IObjectArray of IShellLink & IShellItem to std::vector. +void ConvertRemovedJumpListItems(IObjectArray* in, + std::vector* out) { + DCHECK(in); + DCHECK(out); + + UINT removed_count; + if (SUCCEEDED(in->GetCount(&removed_count) && (removed_count > 0))) { + out->reserve(removed_count); + JumpListItem item; + IShellItem* shell_item; + IShellLink* shell_link; + for (UINT i = 0; i < removed_count; ++i) { + if (SUCCEEDED(in->GetAt(i, IID_PPV_ARGS(&shell_item)))) { + item.type = JumpListItem::Type::FILE; + GetShellItemFileName(shell_item, &item.path); + out->push_back(item); + shell_item->Release(); + } else if (SUCCEEDED(in->GetAt(i, IID_PPV_ARGS(&shell_link)))) { + if (ConvertShellLinkToJumpListItem(shell_link, &item)) + out->push_back(item); + shell_link->Release(); + } + } + } +} + +} // namespace + +namespace atom { + +JumpList::JumpList(const base::string16& app_id) : app_id_(app_id) { + destinations_.CoCreateInstance(CLSID_DestinationList); +} + +bool JumpList::Begin(int* min_items, std::vector* removed_items) { + DCHECK(destinations_); + if (!destinations_) + return false; + + if (FAILED(destinations_->SetAppID(app_id_.c_str()))) + return false; + + UINT min_slots; + CComPtr removed; + if (FAILED(destinations_->BeginList(&min_slots, IID_PPV_ARGS(&removed)))) + return false; + + if (min_items) + *min_items = min_slots; + + if (removed_items) + ConvertRemovedJumpListItems(removed, removed_items); + + return true; +} + +bool JumpList::Abort() { + DCHECK(destinations_); + if (!destinations_) + return false; + + return SUCCEEDED(destinations_->AbortList()); +} + +bool JumpList::Commit() { + DCHECK(destinations_); + if (!destinations_) + return false; + + return SUCCEEDED(destinations_->CommitList()); +} + +bool JumpList::Delete() { + DCHECK(destinations_); + if (!destinations_) + return false; + + return SUCCEEDED(destinations_->DeleteList(app_id_.c_str())); +} + +// This method will attempt to append as many items to the Jump List as +// possible, and will return a single error code even if multiple things +// went wrong in the process. To get detailed information about what went +// wrong enable runtime logging. +JumpListResult JumpList::AppendCategory(const JumpListCategory& category) { + DCHECK(destinations_); + if (!destinations_) + return JumpListResult::GENERIC_ERROR; + + if (category.items.empty()) + return JumpListResult::SUCCESS; + + CComPtr collection; + if (FAILED(collection.CoCreateInstance(CLSID_EnumerableObjectCollection))) { + return JumpListResult::GENERIC_ERROR; + } + + auto result = JumpListResult::SUCCESS; + // Keep track of how many items were actually appended to the category. + int appended_count = 0; + for (const auto& item : category.items) { + switch (item.type) { + case JumpListItem::Type::TASK: + if (AppendTask(item, collection)) + ++appended_count; + else + LOG(ERROR) << "Failed to append task '" << item.title << "' " + "to Jump List."; + break; + + case JumpListItem::Type::SEPARATOR: + if (category.type == JumpListCategory::Type::TASKS) { + if (AppendSeparator(collection)) + ++appended_count; + } else { + LOG(ERROR) << "Can't append separator to Jump List category " + << "'" << category.name << "'. " + << "Separators are only allowed in the standard 'Tasks' " + "Jump List category."; + result = JumpListResult::CUSTOM_CATEGORY_SEPARATOR_ERROR; + } + break; + + case JumpListItem::Type::FILE: + if (AppendFile(item, collection)) + ++appended_count; + else + LOG(ERROR) << "Failed to append '" << item.path.value() << "' " + "to Jump List."; + break; + } + } + + if (appended_count == 0) + return result; + + if ((appended_count < category.items.size()) && + (result == JumpListResult::SUCCESS)) { + result = JumpListResult::GENERIC_ERROR; + } + + CComQIPtr items = collection; + + if (category.type == JumpListCategory::Type::TASKS) { + if (FAILED(destinations_->AddUserTasks(items))) { + LOG(ERROR) << "Failed to append items to the standard Tasks category."; + if (result == JumpListResult::SUCCESS) + result = JumpListResult::GENERIC_ERROR; + } + } else { + auto hr = destinations_->AppendCategory(category.name.c_str(), items); + if (FAILED(hr)) { + if (hr == 0x80040F03) { + LOG(ERROR) << "Failed to append custom category " + << "'" << category.name << "' " + << "to Jump List due to missing file type registration."; + result = JumpListResult::MISSING_FILE_TYPE_REGISTRATION_ERROR; + } else if (hr == E_ACCESSDENIED) { + LOG(ERROR) << "Failed to append custom category " + << "'" << category.name << "' " + << "to Jump List due to system privacy settings."; + result = JumpListResult::CUSTOM_CATEGORY_ACCESS_DENIED_ERROR; + } else { + LOG(ERROR) << "Failed to append custom category " + << "'" << category.name << "' to Jump List."; + if (result == JumpListResult::SUCCESS) + result = JumpListResult::GENERIC_ERROR; + } + } + } + return result; +} + +// This method will attempt to append as many categories to the Jump List +// as possible, and will return a single error code even if multiple things +// went wrong in the process. To get detailed information about what went +// wrong enable runtime logging. +JumpListResult JumpList::AppendCategories( + const std::vector& categories) { + DCHECK(destinations_); + if (!destinations_) + return JumpListResult::GENERIC_ERROR; + + auto result = JumpListResult::SUCCESS; + for (const auto& category : categories) { + auto latestResult = JumpListResult::SUCCESS; + switch (category.type) { + case JumpListCategory::Type::TASKS: + case JumpListCategory::Type::CUSTOM: + latestResult = AppendCategory(category); + break; + + case JumpListCategory::Type::RECENT: + if (FAILED(destinations_->AppendKnownCategory(KDC_RECENT))) { + LOG(ERROR) << "Failed to append Recent category to Jump List."; + latestResult = JumpListResult::GENERIC_ERROR; + } + break; + + case JumpListCategory::Type::FREQUENT: + if (FAILED(destinations_->AppendKnownCategory(KDC_FREQUENT))) { + LOG(ERROR) << "Failed to append Frequent category to Jump List."; + latestResult = JumpListResult::GENERIC_ERROR; + } + break; + } + // Keep the first non-generic error code as only one can be returned from + // the function (so try to make it the most useful one). + if (((result == JumpListResult::SUCCESS) || + (result == JumpListResult::GENERIC_ERROR)) && + (latestResult != JumpListResult::SUCCESS)) + result = latestResult; + } + return result; +} + +} // namespace atom diff --git a/atom/browser/ui/win/jump_list.h b/atom/browser/ui/win/jump_list.h new file mode 100644 index 000000000000..c7660003ae79 --- /dev/null +++ b/atom/browser/ui/win/jump_list.h @@ -0,0 +1,112 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_WIN_JUMP_LIST_H_ +#define ATOM_BROWSER_UI_WIN_JUMP_LIST_H_ + +#include +#include +#include + +#include "base/files/file_path.h" +#include "base/macros.h" + +namespace atom { + +enum class JumpListResult : int { + SUCCESS = 0, + // In JS code this error will manifest as an exception. + ARGUMENT_ERROR = 1, + // Generic error, the runtime logs may provide some clues. + GENERIC_ERROR = 2, + // Custom categories can't contain separators. + CUSTOM_CATEGORY_SEPARATOR_ERROR = 3, + // The app isn't registered to handle a file type found in a custom category. + MISSING_FILE_TYPE_REGISTRATION_ERROR = 4, + // Custom categories can't be created due to user privacy settings. + CUSTOM_CATEGORY_ACCESS_DENIED_ERROR = 5, +}; + +struct JumpListItem { + enum class Type { + // A task will launch an app (usually the one that created the Jump List) + // with specific arguments. + TASK, + // Separators can only be inserted between items in the standard Tasks + // category, they can't appear in custom categories. + SEPARATOR, + // A file link will open a file using the app that created the Jump List, + // for this to work the app must be registered as a handler for the file + // type (though the app doesn't have to be the default handler). + FILE + }; + + Type type = Type::TASK; + // For tasks this is the path to the program executable, for file links this + // is the full filename. + base::FilePath path; + base::string16 arguments; + base::string16 title; + base::string16 description; + base::FilePath icon_path; + int icon_index = 0; +}; + +struct JumpListCategory { + enum class Type { + // A custom category can contain tasks and files, but not separators. + CUSTOM, + // Frequent/Recent categories are managed by the OS, their name and items + // can't be set by the app (though items can be set indirectly). + FREQUENT, + RECENT, + // The standard Tasks category can't be renamed by the app, but the app + // can set the items that should appear in this category, and those items + // can include tasks, files, and separators. + TASKS + }; + + Type type = Type::TASKS; + base::string16 name; + std::vector items; +}; + +// Creates or removes a custom Jump List for an app. +// See https://msdn.microsoft.com/en-us/library/windows/desktop/gg281362.aspx +class JumpList { + public: + // |app_id| must be the Application User Model ID of the app for which the + // custom Jump List should be created/removed, it's usually obtained by + // calling GetCurrentProcessExplicitAppUserModelID(). + explicit JumpList(const base::string16& app_id); + + // Starts a new transaction, must be called before appending any categories, + // aborting or committing. After the method returns |min_items| will indicate + // the minimum number of items that will be displayed in the Jump List, and + // |removed_items| (if not null) will contain all the items the user has + // unpinned from the Jump List. Both parameters are optional. + bool Begin(int* min_items = nullptr, + std::vector* removed_items = nullptr); + // Abandons any changes queued up since Begin() was called. + bool Abort(); + // Commits any changes queued up since Begin() was called. + bool Commit(); + // Deletes the custom Jump List and restores the default Jump List. + bool Delete(); + // Appends a category to the custom Jump List. + JumpListResult AppendCategory(const JumpListCategory& category); + // Appends categories to the custom Jump List. + JumpListResult AppendCategories( + const std::vector& categories); + + private: + base::string16 app_id_; + CComPtr destinations_; + + DISALLOW_COPY_AND_ASSIGN(JumpList); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_UI_WIN_JUMP_LIST_H_ diff --git a/docs/api/app.md b/docs/api/app.md index aeb003d33974..8fa186e66fbd 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -527,6 +527,151 @@ Adds `tasks` to the [Tasks][tasks] category of the JumpList on Windows. Returns `true` when the call succeeded, otherwise returns `false`. +**Note:** If you'd like to customize the Jump List even more use +`app.setJumpList(categories)` instead. + +### `app.getJumpListSettings()` _Windows_ + +Returns an Object with the following properties: + +* `minItems` Integer - The minimum number of items that will be shown in the + Jump List (for a more detailed description of this value see the + [MSDN docs][JumpListBeginListMSDN]). +* `removedItems` Array - Array of `JumpListItem` objects that correspond to + items that the user has explicitly removed from custom categories in the + Jump List. These items must not be re-added to the Jump List in the **next** + call to `app.setJumpList()`, Windows will not display any custom category + that contains any of the removed items. + +### `app.setJumpList(categories)` _Windows_ + +* `categories` Array or `null` - Array of `JumpListCategory` objects. + +Sets or removes a custom Jump List for the application, and returns one of the +following strings: + +* `ok` - Nothing went wrong. +* `error` - One or more errors occured, enable runtime logging to figure out + the likely cause. +* `invalidSeparatorError` - An attempt was made to add a separator to a + custom category in the Jump List. Separators are only allowed in the + standard `Tasks` category. +* `fileTypeRegistrationError` - An attempt was made to add a file link to + the Jump List for a file type the app isn't registered to handle. +* `customCategoryAccessDeniedError` - Custom categories can't be added to the + Jump List due to user privacy or group policy settings. + +If `categories` is `null` the previously set custom Jump List (if any) will be +replaced by the standard Jump List for the app (managed by Windows). + +`JumpListCategory` objects should have the following properties: + +* `type` String - One of the following: + * `tasks` - Items in this category will be placed into the standard `Tasks` + category. There can be only one such category, and it will always be + displayed at the bottom of the Jump List. + * `frequent` - Displays a list of files frequently opened by the app, the + name of the category and its items are set by Windows. + * `recent` - Displays a list of files recently opened by the app, the name + of the category and its items are set by Windows. Items may be added to + this category indirectly using `app.addRecentDocument(path)`. + * `custom` - Displays tasks or file links, `name` must be set by the app. +* `name` String - Must be set if `type` is `custom`, otherwise it should be + omitted. +* `items` Array - Array of `JumpListItem` objects if `type` is `tasks` or + `custom`, otherwise it should be omitted. + +**Note:** If a `JumpListCategory` object has neither the `type` nor the `name` +property set then its `type` is assumed to be `tasks`. If the `name` property +is set but the `type` property is omitted then the `type` is assumed to be +`custom`. + +**Note:** Users can remove items from custom categories, and Windows will not +allow a removed item to be added back into a custom category until **after** +the next successful call to `app.setJumpList(categories)`. Any attempt to +re-add a removed item to a custom category earlier than that will result in the +entire custom category being omitted from the Jump List. The list of removed +items can be obtained using `app.getJumpListSettings()`. + +`JumpListItem` objects should have the following properties: + +* `type` String - One of the following: + * `task` - A task will launch an app with specific arguments. + * `separator` - Can be used to separate items in the standard `Tasks` + category. + * `file` - A file link will open a file using the app that created the + Jump List, for this to work the app must be registered as a handler for + the file type (though it doesn't have to be the default handler). +* `path` String - Path of the file to open, should only be set if `type` is + `file`. +* `program` String - Path of the program to execute, usually you should + specify `process.execPath` which opens the current program. Should only be + set if `type` is `task`. +* `args` String - The command line arguments when `program` is executed. Should + only be set if `type` is `task`. +* `title` String - The text to be displayed for the item in the Jump List. + Should only be set if `type` is `task`. +* `description` String - Description of the task (displayed in a tooltip). + Should only be set if `type` is `task`. +* `iconPath` String - The absolute path to an icon to be displayed in a + Jump List, which can be an arbitrary resource file that contains an icon + (e.g. `.ico`, `.exe`, `.dll`). You can usually specify `process.execPath` to + show the program icon. +* `iconIndex` Integer - The index of the icon in the resource file. If a + resource file contains multiple icons this value can be used to specify the + zero-based index of the icon that should be displayed for this task. If a + resource file contains only one icon, this property should be set to zero. + +Here's a very simple example of creating a custom Jump List: + +```javascript +const {app} = require('electron') + +app.setJumpList([ + { + type: 'custom', + name: 'Recent Projects', + items: [ + { type: 'file', path: 'C:\\Projects\\project1.proj' }, + { type: 'file', path: 'C:\\Projects\\project2.proj' } + ] + }, + { // has a name so `type` is assumed to be "custom" + name: 'Tools', + items: [ + { + type: 'task', title: 'Tool A', + program: process.execPath, args: '--run-tool-a', + icon: process.execPath, iconIndex: 0, + description: 'Runs Tool A' + }, + { + type: 'task', title: 'Tool B', + program: process.execPath, args: '--run-tool-b', + icon: process.execPath, iconIndex: 0, + description: 'Runs Tool B' + } + ] + }, + { type: 'frequent' }, + { // has no name and no type so `type` is assumed to be "tasks" + items: [ + { + type: 'task', title: 'New Project', + program: process.execPath, args: '--new-project', + description: 'Create a new project.' + }, + { type: 'separator' }, + { + type: 'task', title: 'Recover Project', + program: process.execPath, args: '--recover-project', + description: 'Recover Project' + } + ] + } +]) +``` + ### `app.makeSingleInstance(callback)` * `callback` Function @@ -771,3 +916,4 @@ Sets the `image` associated with this dock icon. [handoff]: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/Handoff/HandoffFundamentals/HandoffFundamentals.html [activity-type]: https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSUserActivity_Class/index.html#//apple_ref/occ/instp/NSUserActivity/activityType [unity-requiremnt]: ../tutorial/desktop-environment-integration.md#unity-launcher-shortcuts-linux +[JumpListBeginListMSDN]: https://msdn.microsoft.com/en-us/library/windows/desktop/dd378398(v=vs.85).aspx diff --git a/filenames.gypi b/filenames.gypi index 7335ae9acc07..9e2058249e8b 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -294,6 +294,8 @@ 'atom/browser/ui/views/win_frame_view.h', 'atom/browser/ui/win/atom_desktop_window_tree_host_win.cc', 'atom/browser/ui/win/atom_desktop_window_tree_host_win.h', + 'atom/browser/ui/win/jump_list.cc', + 'atom/browser/ui/win/jump_list.h', 'atom/browser/ui/win/message_handler_delegate.cc', 'atom/browser/ui/win/message_handler_delegate.h', 'atom/browser/ui/win/notify_icon_host.cc',