Browse Source

Extend the custom Jump List API

Add `app.getJumpListSettings()` and `app.setJumpList(callback)` that
make it possible to fully customize the Jump List of an Electron app.
It is now possible to:
- Add tasks to the standard `Tasks` category.
- Add separators to the standard `Tasks` category.
- Add custom categories containing tasks and file links.
- Add system managed Recent/Frequent categories.
- Remove the custom Jump List.
Vadim Macagon 8 years ago
parent
commit
c64294cf60

+ 274 - 0
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<Browser::UserTask> {
     return true;
   }
 };
+
+using atom::JumpListItem;
+using atom::JumpListCategory;
+using atom::JumpListResult;
+
+template<>
+struct Converter<JumpListItem::Type> {
+  static bool FromV8(v8::Isolate* isolate, v8::Local<v8::Value> 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<v8::Value> 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<JumpListItem> {
+  static bool FromV8(v8::Isolate* isolate, v8::Local<v8::Value> 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<v8::Value> 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<JumpListCategory::Type> {
+  static bool FromV8(v8::Isolate* isolate, v8::Local<v8::Value> 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<v8::Value> 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<JumpListCategory> {
+  static bool FromV8(v8::Isolate* isolate, v8::Local<v8::Value> 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<JumpListResult> {
+  static v8::Local<v8::Value> 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<v8::Value> App::GetJumpListSettings() {
+  JumpList jump_list(Browser::Get()->GetAppUserModelID());
+
+  int min_items = 10;
+  std::vector<JumpListItem> 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<v8::Value> val,
+                                mate::Arguments* args) {
+  std::vector<JumpListCategory> 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> 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",

+ 13 - 1
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<v8::Value> GetJumpListSettings();
+
+  // Set or remove a custom Jump List for the application.
+  JumpListResult SetJumpList(v8::Local<v8::Value> val, mate::Arguments* args);
+#endif  // defined(OS_WIN)
+
   std::unique_ptr<ProcessSingleton> process_singleton_;
 
 #if defined(USE_NSS_CERTS)

+ 18 - 40
atom/browser/browser_win.cc

@@ -7,10 +7,10 @@
 #include <windows.h>  // windows.h must be included first
 
 #include <atlbase.h>
-#include <propkey.h>
 #include <shlobj.h>
 #include <shobjidl.h>
 
+#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<UserTask>& tasks) {
-  CComPtr<ICustomDestinationList> destinations;
-  if (FAILED(destinations.CoCreateInstance(CLSID_DestinationList)))
-    return false;
-  if (FAILED(destinations->SetAppID(GetAppUserModelID())))
-    return false;
-
-  // Start a transaction that updates the JumpList of this application.
-  UINT max_slots;
-  CComPtr<IObjectArray> removed;
-  if (FAILED(destinations->BeginList(&max_slots, IID_PPV_ARGS(&removed))))
+  JumpList jump_list(GetAppUserModelID());
+  if (!jump_list.Begin())
     return false;
 
-  CComPtr<IObjectCollection> collection;
-  if (FAILED(collection.CoCreateInstance(CLSID_EnumerableObjectCollection)))
-    return false;
-
-  for (auto& task : tasks) {
-    CComPtr<IShellLink> 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<IPropertyStore> 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<IObjectArray> 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,

+ 332 - 0
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 <propkey.h>  // 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<IShellLink> 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<IPropertyStore> 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<IShellLink> shell_link;
+  if (SUCCEEDED(shell_link.CoCreateInstance(CLSID_ShellLink))) {
+    CComQIPtr<IPropertyStore> 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<IShellItem> 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<wchar_t> 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<IPropertyStore> 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<JumpListItem>* 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<JumpListItem>* removed_items) {
+  DCHECK(destinations_);
+  if (!destinations_)
+    return false;
+
+  if (FAILED(destinations_->SetAppID(app_id_.c_str())))
+    return false;
+
+  UINT min_slots;
+  CComPtr<IObjectArray> 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<IObjectCollection> 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<IObjectArray> 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<JumpListCategory>& 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

+ 112 - 0
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 <atlbase.h>
+#include <shobjidl.h>
+#include <vector>
+
+#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<JumpListItem> 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<JumpListItem>* 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<JumpListCategory>& categories);
+
+ private:
+  base::string16 app_id_;
+  CComPtr<ICustomDestinationList> destinations_;
+
+  DISALLOW_COPY_AND_ASSIGN(JumpList);
+};
+
+}  // namespace atom
+
+#endif  // ATOM_BROWSER_UI_WIN_JUMP_LIST_H_

+ 146 - 0
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

+ 2 - 0
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',