diff options
author | jstritar@chromium.org <jstritar@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-01-24 03:41:02 +0000 |
---|---|---|
committer | jstritar@chromium.org <jstritar@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-01-24 03:41:02 +0000 |
commit | 482816411e1fb5379f7f9f7030f8648128bd95c7 (patch) | |
tree | 6a323e76e9c33bc65c168b8ec6a7d7ea63d5eeb2 | |
parent | 076692a9d28e8634e87db7fe182adcf8a51fc293 (diff) | |
download | chromium_src-482816411e1fb5379f7f9f7030f8648128bd95c7.zip chromium_src-482816411e1fb5379f7f9f7030f8648128bd95c7.tar.gz chromium_src-482816411e1fb5379f7f9f7030f8648128bd95c7.tar.bz2 |
[NTP] Allow reordering of apps via drag and drop.
BUG=53977
TEST=None.
Review URL: http://codereview.chromium.org/6297013
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@72311 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/dom_ui/app_launcher_handler.cc | 16 | ||||
-rw-r--r-- | chrome/browser/dom_ui/app_launcher_handler.h | 3 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_prefs.cc | 11 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_prefs.h | 3 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.html | 1 | ||||
-rw-r--r-- | chrome/browser/resources/new_new_tab.js | 13 | ||||
-rw-r--r-- | chrome/browser/resources/ntp/apps.css | 29 | ||||
-rw-r--r-- | chrome/browser/resources/ntp/apps.js | 257 | ||||
-rw-r--r-- | chrome/browser/resources/ntp/drag_drop_controller.js | 164 | ||||
-rw-r--r-- | chrome/common/notification_type.h | 3 |
10 files changed, 483 insertions, 17 deletions
diff --git a/chrome/browser/dom_ui/app_launcher_handler.cc b/chrome/browser/dom_ui/app_launcher_handler.cc index f1679c7..b5c9142 100644 --- a/chrome/browser/dom_ui/app_launcher_handler.cc +++ b/chrome/browser/dom_ui/app_launcher_handler.cc @@ -157,6 +157,8 @@ void AppLauncherHandler::RegisterMessages() { NewCallback(this, &AppLauncherHandler::HandleHideAppsPromo)); dom_ui_->RegisterMessageCallback("createAppShortcut", NewCallback(this, &AppLauncherHandler::HandleCreateAppShortcut)); + dom_ui_->RegisterMessageCallback("reorderApps", + NewCallback(this, &AppLauncherHandler::HandleReorderApps)); } void AppLauncherHandler::Observe(NotificationType type, @@ -168,6 +170,7 @@ void AppLauncherHandler::Observe(NotificationType type, switch (type.value) { case NotificationType::EXTENSION_LOADED: case NotificationType::EXTENSION_UNLOADED: + case NotificationType::EXTENSION_LAUNCHER_REORDERED: if (dom_ui_->tab_contents()) HandleGetApps(NULL); break; @@ -257,6 +260,8 @@ void AppLauncherHandler::HandleGetApps(const ListValue* args) { NotificationService::AllSources()); registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, NotificationService::AllSources()); + registrar_.Add(this, NotificationType::EXTENSION_LAUNCHER_REORDERED, + NotificationService::AllSources()); } if (pref_change_registrar_.IsEmpty()) { pref_change_registrar_.Init( @@ -387,6 +392,17 @@ void AppLauncherHandler::HandleCreateAppShortcut(const ListValue* args) { browser->profile(), extension); } +void AppLauncherHandler::HandleReorderApps(const ListValue* args) { + std::vector<std::string> extension_ids; + for (size_t i = 0; i < args->GetSize(); ++i) { + std::string value; + if (args->GetString(i, &value)) + extension_ids.push_back(value); + } + + extensions_service_->extension_prefs()->SetAppLauncherOrder(extension_ids); +} + // static void AppLauncherHandler::RecordWebStoreLaunch(bool promo_active) { if (!promo_active) return; diff --git a/chrome/browser/dom_ui/app_launcher_handler.h b/chrome/browser/dom_ui/app_launcher_handler.h index 4940752..cee1aaf 100644 --- a/chrome/browser/dom_ui/app_launcher_handler.h +++ b/chrome/browser/dom_ui/app_launcher_handler.h @@ -71,6 +71,9 @@ class AppLauncherHandler // Callback for the "createAppShortcut" message. void HandleCreateAppShortcut(const ListValue* args); + // Callback for the 'reorderApps" message. + void HandleReorderApps(const ListValue* args); + private: // Records a web store launch in the appropriate histograms. |promo_active| // specifies if the web store promotion was active. diff --git a/chrome/browser/extensions/extension_prefs.cc b/chrome/browser/extensions/extension_prefs.cc index 3f6bb88..33ced74 100644 --- a/chrome/browser/extensions/extension_prefs.cc +++ b/chrome/browser/extensions/extension_prefs.cc @@ -1101,6 +1101,17 @@ int ExtensionPrefs::GetNextAppLaunchIndex() { return max_value + 1; } +void ExtensionPrefs::SetAppLauncherOrder( + const std::vector<std::string>& extension_ids) { + for (size_t i = 0; i < extension_ids.size(); ++i) + SetAppLaunchIndex(extension_ids.at(i), i); + + NotificationService::current()->Notify( + NotificationType::EXTENSION_LAUNCHER_REORDERED, + Source<ExtensionPrefs>(this), + NotificationService::NoDetails()); +} + void ExtensionPrefs::SetUpdateUrlData(const std::string& extension_id, const std::string& data) { DictionaryValue* dictionary = GetExtensionPref(extension_id); diff --git a/chrome/browser/extensions/extension_prefs.h b/chrome/browser/extensions/extension_prefs.h index f02f6ec..828b98b 100644 --- a/chrome/browser/extensions/extension_prefs.h +++ b/chrome/browser/extensions/extension_prefs.h @@ -251,6 +251,9 @@ class ExtensionPrefs { // highest current application launch index found. int GetNextAppLaunchIndex(); + // Sets the order the apps should be displayed in the app launcher. + void SetAppLauncherOrder(const std::vector<std::string>& extension_ids); + // The extension's update URL data. If not empty, the ExtensionUpdater // will append a ap= parameter to the URL when checking if a new version // of the extension is available. diff --git a/chrome/browser/resources/new_new_tab.html b/chrome/browser/resources/new_new_tab.html index e9bd7a1..b0f8ae9 100644 --- a/chrome/browser/resources/new_new_tab.html +++ b/chrome/browser/resources/new_new_tab.html @@ -289,6 +289,7 @@ i18nTemplate.process(document, templateData); <script src="shared/js/cr/ui/context_menu_handler.js"></script> <script src="ntp/util.js"></script> +<script src="ntp/drag_drop_controller.js"></script> <script src="ntp/most_visited.js"></script> <script src="new_new_tab.js"></script> <script src="ntp/apps.js"></script> diff --git a/chrome/browser/resources/new_new_tab.js b/chrome/browser/resources/new_new_tab.js index 877144c..fdc1c61 100644 --- a/chrome/browser/resources/new_new_tab.js +++ b/chrome/browser/resources/new_new_tab.js @@ -187,7 +187,7 @@ function createForeignSession(client, name) { // Sort tabs by MRU order win.tabs.sort(function(a, b) { return a.timestamp < b.timestamp; - }) + }); // Create individual tab information. win.tabs.forEach(function(data) { @@ -205,7 +205,7 @@ function createForeignSession(client, name) { handleIfEnterKey(maybeOpenForeignTab)); winSpan.appendChild(tabEl); - }) + }); // Append the window. stack.appendChild(winSpan); @@ -309,6 +309,7 @@ function handleWindowResize() { if (layoutMode != oldLayoutMode){ mostVisited.useSmallGrid = b; mostVisited.layout(); + apps.layout({force:true}); renderRecentlyClosed(); renderForeignSessions(); updateAllMiniviewClippings(); @@ -626,6 +627,10 @@ function showSection(section) { mostVisited.visible = true; mostVisited.layout(); break; + case Section.APPS: + apps.visible = true; + apps.layout({disableAnimations:true}); + break; } } } @@ -650,6 +655,10 @@ function hideSection(section) { mostVisited.visible = false; mostVisited.layout(); break; + case Section.APPS: + apps.visible = false; + apps.layout(); + break; } var el = getSectionElement(section); diff --git a/chrome/browser/resources/ntp/apps.css b/chrome/browser/resources/ntp/apps.css index 2d03d58..ab129fc 100644 --- a/chrome/browser/resources/ntp/apps.css +++ b/chrome/browser/resources/ntp/apps.css @@ -2,7 +2,6 @@ #apps-content { position: relative; - width: intrinsic; max-width: 780px; /* (124 + margin * 2) * 6 */ } @@ -28,9 +27,8 @@ html.apps-promo-visible #apps-content { -webkit-perspective: 400; border-radius: 10px; color: black; - display: inline-block; margin: 5px 3px; - position: relative; + position: absolute; height: 136px; width: 124px; /* 920 / 7 - margin * 2 */ } @@ -89,6 +87,19 @@ html.apps-promo-visible #apps-content { opacity: .9; } +.app.dragging > .app-settings { + background-image: none; +} + +.app.dragging { + opacity: .7; + z-index: 2; +} + +#apps-content[launcher-animations=true] .app { + -webkit-transition: top .2s, left .2s, right .2s; +} + @-webkit-keyframes bounce { 0% { -webkit-transform: scale(0, 0); @@ -112,7 +123,7 @@ html[install-animation-enabled=true] .app[new=installed] { -webkit-transition: opacity .5s; } -.app[app-id=web-store-entry] > a { +.app.web-store-entry > a { background-image: url("chrome://theme/IDR_WEBSTORE_ICON"); } @@ -154,18 +165,18 @@ html[dir=rtl] #apps-promo-hide { float: left; } -html.apps-promo-visible .app[app-id=web-store-entry] { +html.apps-promo-visible .app.web-store-entry { position: absolute; left: 100%; top: 0; -webkit-margin-start: 22px; } -html.apps-promo-visible[dir=rtl] .app[app-id=web-store-entry] { +html.apps-promo-visible[dir=rtl] .app.web-store-entry { right: 100%; } -html.apps-promo-visible .app[app-id=web-store-entry] a { +html.apps-promo-visible .app.web-store-entry a { font-weight: bold; } @@ -175,12 +186,12 @@ column when there is at least one full row of apps. Note that this is similar, but different than its position during promo (html.apps-promo-visible), so we never set .loner while the promo is running. */ -.app[app-id=web-store-entry].loner { +.app.web-store-entry.loner { position: absolute; left: 100%; top: 0; } -html[dir=rtl] .app[app-id=web-store-entry].loner { +html[dir=rtl] .app.web-store-entry.loner { right: 100%; } diff --git a/chrome/browser/resources/ntp/apps.js b/chrome/browser/resources/ntp/apps.js index e61ebe0..db15e58 100644 --- a/chrome/browser/resources/ntp/apps.js +++ b/chrome/browser/resources/ntp/apps.js @@ -45,6 +45,15 @@ function getAppsCallback(data) { return a.app_launch_index - b.app_launch_index; }); + // Determines if the web store link should be detached and place in the + // top right of the screen. + apps.detachWebstoreEntry = + !apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode]; + + apps.data = data.apps; + if (!apps.detachWebstoreEntry) + apps.data.push('web-store-entry'); + clearClosedMenu(apps.menu); data.apps.forEach(function(app) { appsSectionContent.appendChild(apps.createElement(app)); @@ -84,12 +93,15 @@ function getAppsCallback(data) { appsPromoLink.setAttribute('ping', appsPromoPing); maybeDoneLoading(); - if (isDoneLoading()) { - if (!apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode]) - webStoreEntry.classList.add('loner'); - else - webStoreEntry.classList.remove('loner'); + // Disable the animations when the app launcher is being (re)initailized. + apps.layout({disableAnimations:true}); + + if (apps.detachWebstoreEntry) + webStoreEntry.classList.add('loner'); + else + webStoreEntry.classList.remove('loner'); + if (isDoneLoading()) { updateMiniviewClipping(appsMiniview); layoutSections(); } @@ -261,6 +273,12 @@ var apps = (function() { } }); + // Moves the element at position |from| in array |arr| to position |to|. + function arrayMove(arr, from, to) { + var element = arr.splice(from, 1); + arr.splice(to, 0, element[0]); + } + return { loaded: false, @@ -268,6 +286,230 @@ var apps = (function() { showPromo: false, + detachWebstoreEntry: false, + + // The list of app ids, in order, of each app in the launcher. + data_: null, + get data() { return this.data_; }, + set data(data) { + this.data_ = data.map(function(app) { + return app.id; + }); + this.invalidate_(); + }, + + dirty_: true, + invalidate_: function() { + this.dirty_ = true; + }, + + visible_: true, + get visible() { + return this.visible_; + }, + set visible(visible) { + this.visible_ = visible; + this.invalidate_(); + }, + + // DragAndDropDelegate + + dragContainer: $('apps-content'), + transitionsDuration: 200, + + get dragItem() { return this.dragItem_; }, + set dragItem(dragItem) { + if (this.dragItem_ != dragItem) { + this.dragItem_ = dragItem; + this.invalidate_(); + } + }, + + // The dimensions of each item in the app launcher. This calculates the + // dimensions dynamically, so it should be called after creating the DOM. + dimensions_: null, + get dimensions() { + if (this.dimensions_) + return this.dimensions_; + + var app = this.dragContainer.firstChild; + + var width = app.offsetWidth; + var height = app.offsetHeight; + + // If the apps haven't properly loaded yet, don't cache the result. + if (app.offsetWidth == 0 || app.offsetHeight == 0) + return {width:0, height:0}; + + var style = getComputedStyle(app); + + var marginWidth = + parseInt(style.marginLeft) + parseInt(style.marginRight); + var marginHeight = + parseInt(style.marginTop) + parseInt(style.marginBottom); + + var borderWidth = parseInt(style.borderLeftWidth) + + parseInt(style.borderRightWidth); + var borderHeight = parseInt(style.borderTopWidth) + + parseInt(style.borderBottomWidth); + + this.dimensions_ = { + width: width + marginWidth + borderWidth, + height: height + marginHeight + borderHeight + }; + + return this.dimensions_; + }, + + // Gets the item under the mouse event |e|. Returns null if there is no + // item or if the item is not draggable. + getItem: function(e) { + var item = findAncestorByClass(e.target, 'app'); + + // You can't drag the web store launcher. + if (item.classList.contains('web-store-entry')) + return null; + + return item; + }, + + // Returns true if |coordinates| point to a valid drop location. The + // coordinates are relative to the drag container and the object should + // have the 'x' and 'y' properties set. + canDropOn: function(coordinates) { + var cols = MAX_APPS_PER_ROW[layoutMode]; + var rows = Math.ceil(this.data.length / cols); + + var bottom = rows * this.dimensions.height; + var right = cols * this.dimensions.width; + + if (coordinates.x > right || coordinates.x < 0 || + coordinates.y > bottom || coordinates.y < 0) + return false; + + var position = this.getIndexAt_(coordinates); + var appCount = this.data.length; + + if (!this.detachWebstoreEntry) + appCount--; + + return position >= 0 && position < appCount; + }, + + setDragPlaceholder: function(coordinates) { + var position = this.getIndexAt_(coordinates); + var appId = this.dragItem.querySelector('a').getAttribute('app-id'); + var current = this.data.indexOf(appId); + + if (current == position || current < 0) + return; + + arrayMove(this.data, current, position); + this.invalidate_(); + this.layout(); + }, + + getIndexAt_: function(coordinates) { + var x = coordinates.x; + var y = coordinates.y; + + var w = this.dimensions.width; + var h = this.dimensions.height; + + var availableWidth = this.dragContainer.offsetWidth; + + var row = Math.floor(y / h); + var col = Math.floor(x / w); + var index = Math.floor(availableWidth / w) * row + col; + + return index; + }, + + saveDrag: function() { + this.invalidate_(); + this.layout(); + + var appIds = this.data.filter(function(id) { + return id != 'web-store-entry'; + }); + + // Wait until the transitions are complete before notifying the browser. + // Otherwise, the apps will be re-rendered while still transitioning. + setTimeout(function() { + chrome.send('reorderApps', appIds); + }, this.transitionsDuration + 10); + }, + + layout: function(options) { + options = options || {}; + if (!this.dirty_ && options.force != true) + return; + + try { + var container = this.dragContainer; + if (options.disableAnimations) + container.setAttribute('launcher-animations', false); + var d0 = Date.now(); + this.layoutImpl_(); + this.dirty_ = false; + logEvent('apps.layout: ' + (Date.now() - d0)); + + } finally { + if (options.disableAnimations) { + // We need to re-enable animations asynchronously, so that the + // animations are still disabled for this layout update. + setTimeout(function() { + container.setAttribute('launcher-animations', true); + }, 0); + } + } + }, + + layoutImpl_: function() { + var apps = this.data; + var rects = this.getLayoutRects_(apps.length); + var appsContent = this.dragContainer; + + if (!this.visible) + return; + + for (var i = 0; i < apps.length; i++) { + var app = appsContent.querySelector('[app-id='+apps[i]+']').parentNode; + + // If the node is being dragged, don't try to place it in the grid. + if (app == this.dragItem) + continue; + + app.style.left = rects[i].left + 'px'; + app.style.top = rects[i].top + 'px'; + } + + // We need to set the container's height manually because the apps use + // absolute positioning. + var rows = Math.ceil(apps.length / MAX_APPS_PER_ROW[layoutMode]); + appsContent.style.height = (rows * this.dimensions.height) + 'px'; + }, + + getLayoutRects_: function(appCount) { + var availableWidth = this.dragContainer.offsetWidth; + var rtl = isRtl(); + var rects = []; + var w = this.dimensions.width; + var h = this.dimensions.height; + + for (var i = 0; i < appCount; i++) { + var row = Math.floor((w * i) / availableWidth); + var top = row * h; + var left = (w * i) % availableWidth; + + // Reflect the X axis if an RTL language is active. + if (rtl) + left = availableWidth - left - w; + rects[i] = {left: left, top: top, row: row}; + } + return rects; + }, + createElement: function(app) { var div = createElement(app); var a = div.firstChild; @@ -344,7 +586,7 @@ var apps = (function() { 'name': localStrings.getString('web_store_title'), 'launch_url': localStrings.getString('web_store_url') }); - elm.setAttribute('app-id', 'web-store-entry'); + elm.classList.add('web-store-entry'); return elm; }, @@ -364,3 +606,6 @@ var apps = (function() { } }; })(); + +// Enable drag and drop reordering of the app launcher. +var appDragAndDrop = new DragAndDropController(apps); diff --git a/chrome/browser/resources/ntp/drag_drop_controller.js b/chrome/browser/resources/ntp/drag_drop_controller.js new file mode 100644 index 0000000..fe604a4 --- /dev/null +++ b/chrome/browser/resources/ntp/drag_drop_controller.js @@ -0,0 +1,164 @@ +// Copyright (c) 2011 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. + +// The delegate interface: +// dragContainer --> +// element containing the draggable items +// +// transitionsDuration --> +// length of time of transitions in ms +// +// dragItem --> +// get / set property containing the item being dragged +// +// getItem(e) --> +// get's the item that is under the mouse event |e| +// +// canDropOn(coordinates) --> +// returns true if the coordinates (relative to the drag container) +// point to a valid place to drop an item +// +// setDragPlaceholder(coordinates) --> +// tells the delegate that the dragged item is currently above +// the specified coordinates. +// +// saveDrag() --> +// tells the delegate that the drag is done. move the item to the +// position last specified by setDragPlaceholder. (e.g., commit changes) +// + +function DragAndDropController(delegate) { + this.delegate_ = delegate; + + this.installHandlers_(); +} + +DragAndDropController.prototype = { + startX_: 0, + startY_: 0, + startScreenX_: 0, + startScreenY_: 0, + + installHandlers_: function() { + var el = this.delegate_.dragContainer; + el.addEventListener('dragstart', this.handleDragStart_.bind(this)); + el.addEventListener('dragenter', this.handleDragEnter_.bind(this)); + el.addEventListener('dragover', this.handleDragOver_.bind(this)); + el.addEventListener('dragleave', this.handleDragLeave_.bind(this)); + el.addEventListener('drop', this.handleDrop_.bind(this)); + el.addEventListener('dragend', this.handleDragEnd_.bind(this)); + el.addEventListener('drag', this.handleDrag_.bind(this)); + el.addEventListener('mousedown', this.handleMouseDown_.bind(this)); + }, + + getCoordinates_: function(e) { + var rect = this.delegate_.dragContainer.getBoundingClientRect(); + var coordinates = { + x: e.clientX + window.scrollX - rect.left, + y: e.clientY + window.scrollY - rect.top + }; + + // If we're in an RTL language, reflect the coordinates so the delegate + // doesn't need to worry about it. + if (isRtl()) + coordinates.x = this.delegate_.dragContainer.offsetWidth - coordinates.x; + + return coordinates; + }, + + // Listen to mousedown to get the relative position of the cursor when + // starting drag and drop. + handleMouseDown_: function(e) { + var item = this.delegate_.getItem(e); + if (!item) + return; + + this.startX_ = item.offsetLeft; + this.startY_ = item.offsetTop; + this.startScreenX_ = e.screenX; + this.startScreenY_ = e.screenY; + + // We don't want to focus the item on mousedown. However, to prevent + // focus one has to call preventDefault but this also prevents the drag + // and drop (sigh) so we only prevent it when the user is not doing a + // left mouse button drag. + if (e.button != 0) // LEFT + e.preventDefault(); + }, + + handleDragStart_: function(e) { + var item = this.delegate_.getItem(e); + if (!item) + return; + + // Don't set data since HTML5 does not allow setting the name for + // url-list. Instead, we just rely on the dragging of link behavior. + this.delegate_.dragItem = item; + item.classList.add('dragging'); + + e.dataTransfer.effectAllowed = 'copyLinkMove'; + }, + + handleDragEnter_: function(e) { + if (this.delegate_.canDropOn(this.getCoordinates_(e))) + e.preventDefault(); + }, + + handleDragOver_: function(e) { + var coordinates = this.getCoordinates_(e); + if (!this.delegate_.canDropOn(coordinates)) + return; + + this.delegate_.setDragPlaceholder(coordinates); + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }, + + handleDragLeave_: function(e) { + if (this.delegate_.canDropOn(this.getCoordinates_(e))) + e.preventDefault(); + }, + + handleDrop_: function(e) { + var dragItem = this.delegate_.dragItem; + if (!dragItem) + return; + + this.delegate_.dragItem = null; + this.delegate_.saveDrag(); + + setTimeout(function() { + dragItem.classList.remove('dragging'); + }, this.delegate_.transitionsDuration + 10); + }, + + handleDragEnd_: function(e) { + return this.handleDrop_(e); + }, + + handleDrag_: function(e) { + // Moves the drag item making sure that it is not displayed outside the + // browser viewport. + var dragItem = this.delegate_.dragItem; + var rect = this.delegate_.dragContainer.getBoundingClientRect(); + + var x = this.startX_ + e.screenX - this.startScreenX_; + var y = this.startY_ + e.screenY - this.startScreenY_; + + // The position of the item is relative to #apps so we need to + // subtract that when calculating the allowed position. + x = Math.max(x, -rect.left); + x = Math.min(x, document.body.clientWidth - rect.left - + dragItem.offsetWidth - 2); + + // The shadow is 2px + y = Math.max(-rect.top, y); + y = Math.min(y, document.body.clientHeight - rect.top - + dragItem.offsetHeight - 2); + + // Override right in case of RTL. + dragItem.style.left = x + 'px'; + dragItem.style.top = y + 'px'; + } +}; diff --git a/chrome/common/notification_type.h b/chrome/common/notification_type.h index 54f2a36..fadff2f 100644 --- a/chrome/common/notification_type.h +++ b/chrome/common/notification_type.h @@ -1305,6 +1305,9 @@ class NotificationType { // |webkit_glue::PasswordForm|s that were affected. LOGINS_CHANGED, + // Sent when the applications in the NTP app launcher have been reordered. + EXTENSION_LAUNCHER_REORDERED, + // Count (must be last) ---------------------------------------------------- // Used to determine the number of notification types. Not valid as // a type parameter when registering for or posting notifications. |