summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build/common.gypi10
-rw-r--r--chrome/app/generated_resources.grd31
-rw-r--r--chrome/browser/intents/web_intents_registry.h1
-rw-r--r--chrome/browser/intents/web_intents_registry_factory.h2
-rw-r--r--chrome/browser/resources/options/content_settings.html50
-rw-r--r--chrome/browser/resources/options/content_settings.js14
-rw-r--r--chrome/browser/resources/options/intents_list.js715
-rw-r--r--chrome/browser/resources/options/intents_view.css181
-rw-r--r--chrome/browser/resources/options/intents_view.html12
-rw-r--r--chrome/browser/resources/options/intents_view.js83
-rw-r--r--chrome/browser/resources/options/options.html6
-rw-r--r--chrome/browser/resources/options/options.js6
-rw-r--r--chrome/browser/resources/options/options_bundle.js4
-rw-r--r--chrome/browser/ui/intents/intents_model.cc162
-rw-r--r--chrome/browser/ui/intents/intents_model.h128
-rw-r--r--chrome/browser/ui/intents/intents_model_unittest.cc185
-rw-r--r--chrome/browser/ui/webui/options/intents_settings_handler.cc159
-rw-r--r--chrome/browser/ui/webui/options/intents_settings_handler.h78
-rw-r--r--chrome/browser/ui/webui/options/options_ui.cc2
-rw-r--r--chrome/chrome_browser.gypi4
-rw-r--r--chrome/chrome_tests.gypi1
21 files changed, 1803 insertions, 31 deletions
diff --git a/build/common.gypi b/build/common.gypi
index d5862d9..ea01182 100644
--- a/build/common.gypi
+++ b/build/common.gypi
@@ -327,6 +327,7 @@
'asan%': '<(asan)',
'enable_register_protocol_handler%': '<(enable_register_protocol_handler)',
'enable_smooth_scrolling%': '<(enable_smooth_scrolling)',
+ 'enable_web_intents%': '<(enable_web_intents)',
# Whether to build for Wayland display server
'use_wayland%': 0,
@@ -716,6 +717,10 @@
'grit_defines': ['-D', 'enable_register_protocol_handler'],
}],
+ ['enable_web_intents==1', {
+ 'grit_defines': ['-D', 'enable_web_intents'],
+ }],
+
['asan==1', {
'clang%': 1,
}],
@@ -935,6 +940,11 @@
'ENABLE_REGISTER_PROTOCOL_HANDLER=1',
],
}],
+ ['enable_web_intents==1', {
+ 'defines': [
+ 'ENABLE_INTENTS=1',
+ ],
+ }],
], # conditions for 'target_defaults'
'target_conditions': [
['chromium_code==0', {
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index daaa708..ec1154b 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -5497,6 +5497,9 @@ Keep your key file in a safe place. You will need it to create new versions of y
<message name="IDS_COOKIES_BLOCK_3RDPARTY_CHKBOX" desc="A checkbox in the Content Settings dialog for blocking all 3rd party cookies.">
Block third-party cookies from being set
</message>
+ <message name="IDS_INTENTS_TAB_LABEL" desc="Label for Web Intents tab on Content Settings page">
+ Web Intents
+ </message>
<!-- Mac users do not close their browser; they quit it. -->
<if expr="not is_macosx">
@@ -5649,9 +5652,6 @@ Keep your key file in a safe place. You will need it to create new versions of y
<message name="IDS_NOTIFICATIONS_ASK_RADIO" desc="A radio button in Content Settings dialog to configure notifications per-site.">
Ask me when a site wants to show desktop notifications (recommended)
</message>
- <message name="IDS_INTENTS_TAB_LABEL" desc="Label for Intents Windows tab on Content Settings dialog">
- Web Intents
- </message>
<message name="IDS_INTENTS_HEADER" desc="Header for intent exception management page on Content Settings dialog">
Web Intents Exceptions
</message>
@@ -5739,6 +5739,17 @@ Keep your key file in a safe place. You will need it to create new versions of y
</message>
</if>
+ <if expr="pp_ifdef('use_titlecase')">
+ <message name="IDS_INTENTS_MANAGE_BUTTON" desc="">
+ Manage Intents...
+ </message>
+ </if>
+ <if expr="not pp_ifdef('use_titlecase')">
+ <message name="IDS_INTENTS_MANAGE_BUTTON" desc="">
+ Manage intents...
+ </message>
+ </if>
+
<!-- Cookie/Images/JavaScript/Plugins/Pop-ups/Location exception editor dialogs -->
<message name="IDS_EXCEPTION_EDITOR_PATTERN_TITLE" desc="Title of the pattern field in the exception editor">
Hostname Pattern:
@@ -8179,6 +8190,20 @@ Keep your key file in a safe place. You will need it to create new versions of y
Expires:
</message>
+ <!-- Intents Window -->
+ <message name="IDS_INTENTS_MANAGER_WINDOW_TITLE" desc="The title of the Intents page that lets you manage intent services for websites">
+ Intents
+ </message>
+ <message name="IDS_INTENTS_DOMAIN_COLUMN_HEADER" desc="Label for the Site column in the intents manager page.">
+ Site
+ </message>
+ <message name="IDS_INTENTS_SERVICE_DATA_COLUMN_HEADER" desc="Label for the Service Data column in the intents manager page.">
+ Service Data
+ </message>
+ <message name="IDS_INTENTS_REMOVE_INTENT_BUTTON" desc="Text on the button to remove an intent.">
+ Remove Intent
+ </message>
+
<!-- Mac users do not close their browser; they quit it. -->
<if expr="not is_macosx">
<message name="IDS_COOKIES_COOKIE_EXPIRES_SESSION" desc="The Cookie Expires field value for a session cookie">
diff --git a/chrome/browser/intents/web_intents_registry.h b/chrome/browser/intents/web_intents_registry.h
index f5d724a..4b0f646 100644
--- a/chrome/browser/intents/web_intents_registry.h
+++ b/chrome/browser/intents/web_intents_registry.h
@@ -55,6 +55,7 @@ class WebIntentsRegistry
// WebIntentsRegistry.
friend class WebIntentsRegistryFactory;
friend class WebIntentsRegistryTest;
+ friend class IntentsModelTest;
WebIntentsRegistry();
virtual ~WebIntentsRegistry();
diff --git a/chrome/browser/intents/web_intents_registry_factory.h b/chrome/browser/intents/web_intents_registry_factory.h
index 4827097..05ea36b8 100644
--- a/chrome/browser/intents/web_intents_registry_factory.h
+++ b/chrome/browser/intents/web_intents_registry_factory.h
@@ -18,7 +18,7 @@ class WebIntentsRegistry;
class WebIntentsRegistryFactory : public ProfileKeyedServiceFactory {
public:
// Returns the WebIntentsRegistry that provides intent registration for
- // |profile|.
+ // |profile|. Ownership stays with this factory object.
static WebIntentsRegistry* GetForProfile(Profile* profile);
// Returns the singleton instance of the WebIntentsRegistryFactory.
diff --git a/chrome/browser/resources/options/content_settings.html b/chrome/browser/resources/options/content_settings.html
index ea1fb11..9579633 100644
--- a/chrome/browser/resources/options/content_settings.html
+++ b/chrome/browser/resources/options/content_settings.html
@@ -212,30 +212,34 @@
</div>
</section>
<!-- Intent registration filter tab contents -->
- <section id="intent-filter">
+ <if expr="pp_ifdef('enable_web_intents')">
+ <section id="intents-section">
<h3 i18n-content="intentsTabLabel" class="content-settings-header"></h3>
- <div>
- <div class="radio">
- <label>
- <input type="radio" name="intents" value="allow">
- <span i18n-content="intentsAllow"></span>
- </label>
- </div>
- <div class="radio">
- <label>
- <input type="radio" name="intents" value="ask">
- <span i18n-content="intentsAsk"></span>
- </label>
- </div>
- <div class="radio">
- <label>
- <input type="radio" name="intents" value="block">
- <span i18n-content="intentsBlock"></span>
- </label>
+ <div>
+ <div class="radio">
+ <label>
+ <input type="radio" name="intents" value="allow">
+ <span i18n-content="intentsAllow"></span>
+ </label>
+ </div>
+ <div class="radio">
+ <label>
+ <input type="radio" name="intents" value="ask">
+ <span i18n-content="intentsAsk"></span>
+ </label>
+ </div>
+ <div class="radio">
+ <label>
+ <input type="radio" name="intents" value="block">
+ <span i18n-content="intentsBlock"></span>
+ </label>
+ </div>
+ <button class="exceptions-list-button" contentType="intents"
+ i18n-content="manage_exceptions"></button>
+ <button id="manage-intents-button" contentType="intents"
+ i18n-content="manageIntents"></button>
</div>
- <button class="exceptions-list-button" contentType="intents"
- i18n-content="manage_exceptions"></button>
- </div>
- </section>
+ </section>
+ </if>
</div>
</div>
diff --git a/chrome/browser/resources/options/content_settings.js b/chrome/browser/resources/options/content_settings.js
index 08c6015..b5bd521 100644
--- a/chrome/browser/resources/options/content_settings.js
+++ b/chrome/browser/resources/options/content_settings.js
@@ -45,14 +45,20 @@ cr.define('options', function() {
};
}
- var manageHandlersButton =
- this.pageDiv.querySelector('#manage-handlers-button');
+ var manageHandlersButton = $('manage-handlers-button');
if (manageHandlersButton) {
manageHandlersButton.onclick = function(event) {
OptionsPage.navigateToPage('handlers');
};
}
+ var manageIntentsButton = $('manage-intents-button');
+ if (manageIntentsButton) {
+ manageIntentsButton.onclick = function(event) {
+ OptionsPage.navigateToPage('intents');
+ };
+ }
+
// Cookies filter page ---------------------------------------------------
$('show-cookies-button').onclick = function(event) {
chrome.send('coreOptionsUserMetricsAction', ['Options_ShowCookies']);
@@ -62,8 +68,8 @@ cr.define('options', function() {
if (!templateData.enable_click_to_play)
$('click_to_play').hidden = true;
- if (!templateData.enable_web_intents)
- $('intent-filter').hidden = true;
+ if (!templateData.enable_web_intents && $('intent-section'))
+ $('intent-section').hidden = true;
},
};
diff --git a/chrome/browser/resources/options/intents_list.js b/chrome/browser/resources/options/intents_list.js
new file mode 100644
index 0000000..d4f3fdd
--- /dev/null
+++ b/chrome/browser/resources/options/intents_list.js
@@ -0,0 +1,715 @@
+// 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.
+
+// TODO(gbillock): refactor this together with CookiesList once we have
+// a better sense from UX design what it'll look like and so what'll be shared.
+cr.define('options', function() {
+ const DeletableItemList = options.DeletableItemList;
+ const DeletableItem = options.DeletableItem;
+ const ArrayDataModel = cr.ui.ArrayDataModel;
+ const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
+ const localStrings = new LocalStrings();
+
+ /**
+ * Returns the item's height, like offsetHeight but such that it works better
+ * when the page is zoomed. See the similar calculation in @{code cr.ui.List}.
+ * This version also accounts for the animation done in this file.
+ * @param {Element} item The item to get the height of.
+ * @return {number} The height of the item, calculated with zooming in mind.
+ */
+ function getItemHeight(item) {
+ var height = item.style.height;
+ // Use the fixed animation target height if set, in case the element is
+ // currently being animated and we'd get an intermediate height below.
+ if (height && height.substr(-2) == 'px')
+ return parseInt(height.substr(0, height.length - 2));
+ return item.getBoundingClientRect().height;
+ }
+
+ // Map of parent pathIDs to node objects.
+ var parentLookup = {};
+
+ // Pending requests for child information.
+ var lookupRequests = {};
+
+ /**
+ * Creates a new list item for intent service data. Note that these are
+ * created and destroyed lazily as they scroll into and out of view,
+ * so they must be stateless. We cache the expanded item in
+ * @{code IntentsList} though, so it can keep state.
+ * (Mostly just which item is selected.)
+ *
+ * @param {Object} origin Data used to create an intents list item.
+ * @param {IntentsList} list The list that will contain this item.
+ * @constructor
+ * @extends {DeletableItem}
+ */
+ function IntentsListItem(origin, list) {
+ var listItem = new DeletableItem(null);
+ listItem.__proto__ = IntentsListItem.prototype;
+
+ listItem.origin = origin;
+ listItem.list = list;
+ listItem.decorate();
+
+ // This hooks up updateOrigin() to the list item, makes the top-level
+ // tree nodes (i.e., origins) register their IDs in parentLookup, and
+ // causes them to request their children if they have none. Note that we
+ // have special logic in the setter for the parent property to make sure
+ // that we can still garbage collect list items when they scroll out of
+ // view, even though it appears that we keep a direct reference.
+ if (origin) {
+ origin.parent = listItem;
+ origin.updateOrigin();
+ }
+
+ return listItem;
+ }
+
+ IntentsListItem.prototype = {
+ __proto__: DeletableItem.prototype,
+
+ /** @inheritDoc */
+ decorate: function() {
+ this.siteChild = this.ownerDocument.createElement('div');
+ this.siteChild.className = 'intents-site';
+ this.dataChild = this.ownerDocument.createElement('div');
+ this.dataChild.className = 'intents-data';
+ this.itemsChild = this.ownerDocument.createElement('div');
+ this.itemsChild.className = 'intents-items';
+ this.infoChild = this.ownerDocument.createElement('div');
+ this.infoChild.className = 'intents-details';
+ this.infoChild.hidden = true;
+ var remove = this.ownerDocument.createElement('button');
+ remove.textContent = localStrings.getString('removeIntent');
+ remove.onclick = this.removeIntent_.bind(this);
+ this.infoChild.appendChild(remove);
+ var content = this.contentElement;
+ content.appendChild(this.siteChild);
+ content.appendChild(this.dataChild);
+ content.appendChild(this.itemsChild);
+ this.itemsChild.appendChild(this.infoChild);
+ if (this.origin && this.origin.data) {
+ this.siteChild.textContent = this.origin.data.site;
+ this.siteChild.setAttribute('title', this.origin.data.site);
+ }
+ this.itemList_ = [];
+ },
+
+ /** @type {boolean} */
+ get expanded() {
+ return this.expanded_;
+ },
+ set expanded(expanded) {
+ if (this.expanded_ == expanded)
+ return;
+ this.expanded_ = expanded;
+ if (expanded) {
+ var oldExpanded = this.list.expandedItem;
+ this.list.expandedItem = this;
+ this.updateItems_();
+ if (oldExpanded)
+ oldExpanded.expanded = false;
+ this.classList.add('show-items');
+ this.dataChild.hidden = true;
+ } else {
+ if (this.list.expandedItem == this) {
+ this.list.leadItemHeight = 0;
+ this.list.expandedItem = null;
+ }
+ this.style.height = '';
+ this.itemsChild.style.height = '';
+ this.classList.remove('show-items');
+ this.dataChild.hidden = false;
+ }
+ },
+
+ /**
+ * The callback for the "remove" button shown when an item is selected.
+ * Requests that the currently selected intent service be removed.
+ * @private
+ */
+ removeIntent_: function() {
+ if (this.selectedIndex_ >= 0) {
+ var item = this.itemList_[this.selectedIndex_];
+ if (item && item.node)
+ chrome.send('removeIntent', [item.node.pathId]);
+ }
+ },
+
+ /**
+ * Disable animation within this intents list item, in preparation for
+ * making changes that will need to be animated. Makes it possible to
+ * measure the contents without displaying them, to set animation targets.
+ * @private
+ */
+ disableAnimation_: function() {
+ this.itemsHeight_ = getItemHeight(this.itemsChild);
+ this.classList.add('measure-items');
+ },
+
+ /**
+ * Enable animation after changing the contents of this intents list item.
+ * See @{code disableAnimation_}.
+ * @private
+ */
+ enableAnimation_: function() {
+ if (!this.classList.contains('measure-items'))
+ this.disableAnimation_();
+ this.itemsChild.style.height = '';
+ // This will force relayout in order to calculate the new heights.
+ var itemsHeight = getItemHeight(this.itemsChild);
+ var fixedHeight = getItemHeight(this) + itemsHeight - this.itemsHeight_;
+ this.itemsChild.style.height = this.itemsHeight_ + 'px';
+ // Force relayout before enabling animation, so that if we have
+ // changed things since the last layout, they will not be animated
+ // during subsequent layouts.
+ this.itemsChild.offsetHeight;
+ this.classList.remove('measure-items');
+ this.itemsChild.style.height = itemsHeight + 'px';
+ this.style.height = fixedHeight + 'px';
+ if (this.expanded)
+ this.list.leadItemHeight = fixedHeight;
+ },
+
+ /**
+ * Updates the origin summary to reflect changes in its items.
+ * Both IntentsListItem and IntentsTreeNode implement this API.
+ * This implementation scans the descendants to update the text.
+ */
+ updateOrigin: function() {
+ console.log('IntentsListItem.updateOrigin');
+ var text = '';
+ for (var i = 0; i < this.origin.children.length; ++i) {
+ if (text.length > 0)
+ text += ', ' + this.origin.children[i].data.action;
+ else
+ text = this.origin.children[i].data.action;
+ }
+ this.dataChild.textContent = text;
+
+ if (this.expanded)
+ this.updateItems_();
+ },
+
+ /**
+ * Updates the items section to reflect changes, animating to the new state.
+ * Removes existing contents and calls @{code IntentsTreeNode.createItems}.
+ * @private
+ */
+ updateItems_: function() {
+ this.disableAnimation_();
+ this.itemsChild.textContent = '';
+ this.infoChild.hidden = true;
+ this.selectedIndex_ = -1;
+ this.itemList_ = [];
+ if (this.origin)
+ this.origin.createItems(this);
+ this.itemsChild.appendChild(this.infoChild);
+ this.enableAnimation_();
+ },
+
+ /**
+ * Append a new intents node "bubble" to this list item.
+ * @param {IntentsTreeNode} node The intents node to add a bubble for.
+ * @param {Element} div The DOM element for the bubble itself.
+ * @return {number} The index the bubble was added at.
+ */
+ appendItem: function(node, div) {
+ this.itemList_.push({node: node, div: div});
+ this.itemsChild.appendChild(div);
+ return this.itemList_.length - 1;
+ },
+
+ /**
+ * The currently selected intents node ("intents bubble") index.
+ * @type {number}
+ * @private
+ */
+ selectedIndex_: -1,
+
+ /**
+ * Get the currently selected intents node ("intents bubble") index.
+ * @type {number}
+ */
+ get selectedIndex() {
+ return this.selectedIndex_;
+ },
+
+ /**
+ * Set the currently selected intents node ("intents bubble") index to
+ * @{code itemIndex}, unselecting any previously selected node first.
+ * @param {number} itemIndex The index to set as the selected index.
+ * TODO: KILL THIS
+ */
+ set selectedIndex(itemIndex) {
+ // Get the list index up front before we change anything.
+ var index = this.list.getIndexOfListItem(this);
+ // Unselect any previously selected item.
+ if (this.selectedIndex_ >= 0) {
+ var item = this.itemList_[this.selectedIndex_];
+ if (item && item.div)
+ item.div.removeAttribute('selected');
+ }
+ // Special case: decrementing -1 wraps around to the end of the list.
+ if (itemIndex == -2)
+ itemIndex = this.itemList_.length - 1;
+ // Check if we're going out of bounds and hide the item details.
+ if (itemIndex < 0 || itemIndex >= this.itemList_.length) {
+ this.selectedIndex_ = -1;
+ this.disableAnimation_();
+ this.infoChild.hidden = true;
+ this.enableAnimation_();
+ return;
+ }
+ // Set the new selected item and show the item details for it.
+ this.selectedIndex_ = itemIndex;
+ this.itemList_[itemIndex].div.setAttribute('selected', '');
+ this.disableAnimation_();
+ this.infoChild.hidden = false;
+ this.enableAnimation_();
+ // If we're near the bottom of the list this may cause the list item to go
+ // beyond the end of the visible area. Fix it after the animation is done.
+ var list = this.list;
+ window.setTimeout(function() { list.scrollIndexIntoView(index); }, 150);
+ },
+ };
+
+ /**
+ * {@code IntentsTreeNode}s mirror the structure of the intents tree lazily,
+ * and contain all the actual data used to generate the
+ * {@code IntentsListItem}s.
+ * @param {Object} data The data object for this node.
+ * @constructor
+ */
+ function IntentsTreeNode(data) {
+ this.data = data;
+ this.children = [];
+ }
+
+ IntentsTreeNode.prototype = {
+ /**
+ * Insert an intents tree node at the given index.
+ * Both IntentsList and IntentsTreeNode implement this API.
+ * @param {Object} data The data object for the node to add.
+ * @param {number} index The index at which to insert the node.
+ */
+ insertAt: function(data, index) {
+ console.log('IntentsTreeNode.insertAt adding ' +
+ JSON.stringify(data) + ' at ' + index);
+ var child = new IntentsTreeNode(data);
+ this.children.splice(index, 0, child);
+ child.parent = this;
+ this.updateOrigin();
+ },
+
+ /**
+ * Remove an intents tree node from the given index.
+ * Both IntentsList and IntentsTreeNode implement this API.
+ * @param {number} index The index of the tree node to remove.
+ */
+ remove: function(index) {
+ if (index < this.children.length) {
+ this.children.splice(index, 1);
+ this.updateOrigin();
+ }
+ },
+
+ /**
+ * Clears all children.
+ * Both IntentsList and IntentsTreeNode implement this API.
+ * It is used by IntentsList.loadChildren().
+ */
+ clear: function() {
+ // We might leave some garbage in parentLookup for removed children.
+ // But that should be OK because parentLookup is cleared when we
+ // reload the tree.
+ this.children = [];
+ this.updateOrigin();
+ },
+
+ /**
+ * The counter used by startBatchUpdates() and endBatchUpdates().
+ * @type {number}
+ */
+ batchCount_: 0,
+
+ /**
+ * See cr.ui.List.startBatchUpdates().
+ * Both IntentsList (via List) and IntentsTreeNode implement this API.
+ */
+ startBatchUpdates: function() {
+ this.batchCount_++;
+ },
+
+ /**
+ * See cr.ui.List.endBatchUpdates().
+ * Both IntentsList (via List) and IntentsTreeNode implement this API.
+ */
+ endBatchUpdates: function() {
+ if (!--this.batchCount_)
+ this.updateOrigin();
+ },
+
+ /**
+ * Requests updating the origin summary to reflect changes in this item.
+ * Both IntentsListItem and IntentsTreeNode implement this API.
+ */
+ updateOrigin: function() {
+ if (!this.batchCount_ && this.parent)
+ this.parent.updateOrigin();
+ },
+
+ /**
+ * Create the intents services rows for this node.
+ * Append the rows to @{code item}.
+ * @param {IntentsListItem} item The intents list item to create items in.
+ */
+ createItems: function(item) {
+ if (this.children.length > 0) {
+ for (var i = 0; i < this.children.length; ++i)
+ this.children[i].createItems(item);
+ } else if (this.data && !this.data.hasChildren) {
+ var div = item.ownerDocument.createElement('div');
+ div.className = 'intents-item';
+ // Help out screen readers and such: this is a clickable thing.
+ div.setAttribute('role', 'button');
+
+ var divAction = item.ownerDocument.createElement('div');
+ divAction.className = 'intents-item-action';
+ divAction.textContent = this.data.action;
+ div.appendChild(divAction);
+
+ var divTypes = item.ownerDocument.createElement('div');
+ divTypes.className = 'intents-item-types';
+ var text = "";
+ for (var i = 0; i < this.data.types.length; ++i) {
+ if (text != "")
+ text += ", ";
+ text += this.data.types[i];
+ }
+ divTypes.textContent = text;
+ div.appendChild(divTypes);
+
+ var divUrl = item.ownerDocument.createElement('div');
+ divUrl.className = 'intents-item-url';
+ divUrl.textContent = this.data.url;
+ div.appendChild(divUrl);
+
+ var index = item.appendItem(this, div);
+ div.onclick = function() {
+ if (item.selectedIndex == index)
+ item.selectedIndex = -1;
+ else
+ item.selectedIndex = index;
+ };
+ }
+ },
+
+ /**
+ * The parent of this intents tree node.
+ * @type {?IntentsTreeNode|IntentsListItem}
+ */
+ get parent(parent) {
+ // See below for an explanation of this special case.
+ if (typeof this.parent_ == 'number')
+ return this.list_.getListItemByIndex(this.parent_);
+ return this.parent_;
+ },
+ set parent(parent) {
+ if (parent == this.parent)
+ return;
+
+ if (parent instanceof IntentsListItem) {
+ // If the parent is to be a IntentsListItem, then we keep the reference
+ // to it by its containing list and list index, rather than directly.
+ // This allows the list items to be garbage collected when they scroll
+ // out of view (except the expanded item, which we cache). This is
+ // transparent except in the setter and getter, where we handle it.
+ this.parent_ = parent.listIndex;
+ this.list_ = parent.list;
+ parent.addEventListener('listIndexChange',
+ this.parentIndexChanged_.bind(this));
+ } else {
+ this.parent_ = parent;
+ }
+
+
+ if (parent)
+ parentLookup[this.pathId] = this;
+ else
+ delete parentLookup[this.pathId];
+
+ if (this.data && this.data.hasChildren &&
+ !this.children.length && !lookupRequests[this.pathId]) {
+ console.log('SENDING loadIntents');
+ lookupRequests[this.pathId] = true;
+ chrome.send('loadIntents', [this.pathId]);
+ }
+ },
+
+ /**
+ * Called when the parent is a IntentsListItem whose index has changed.
+ * See the code above that avoids keeping a direct reference to
+ * IntentsListItem parents, to allow them to be garbage collected.
+ * @private
+ */
+ parentIndexChanged_: function(event) {
+ if (typeof this.parent_ == 'number') {
+ this.parent_ = event.newValue;
+ // We set a timeout to update the origin, rather than doing it right
+ // away, because this callback may occur while the list items are
+ // being repopulated following a scroll event. Calling updateOrigin()
+ // immediately could trigger relayout that would reset the scroll
+ // position within the list, among other things.
+ window.setTimeout(this.updateOrigin.bind(this), 0);
+ }
+ },
+
+ /**
+ * The intents tree path id.
+ * @type {string}
+ */
+ get pathId() {
+ var parent = this.parent;
+ if (parent && parent instanceof IntentsTreeNode)
+ return parent.pathId + ',' + this.data.action;
+ return this.data.site;
+ },
+ };
+
+ /**
+ * Creates a new intents list.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {DeletableItemList}
+ */
+ var IntentsList = cr.ui.define('list');
+
+ IntentsList.prototype = {
+ __proto__: DeletableItemList.prototype,
+
+ /** @inheritDoc */
+ decorate: function() {
+ DeletableItemList.prototype.decorate.call(this);
+ this.classList.add('intents-list');
+ this.data_ = [];
+ this.dataModel = new ArrayDataModel(this.data_);
+ this.addEventListener('keydown', this.handleKeyLeftRight_.bind(this));
+ var sm = new ListSingleSelectionModel();
+ sm.addEventListener('change', this.cookieSelectionChange_.bind(this));
+ sm.addEventListener('leadIndexChange', this.cookieLeadChange_.bind(this));
+ this.selectionModel = sm;
+ },
+
+ /**
+ * Handles key down events and looks for left and right arrows, then
+ * dispatches to the currently expanded item, if any.
+ * @param {Event} e The keydown event.
+ * @private
+ */
+ handleKeyLeftRight_: function(e) {
+ var id = e.keyIdentifier;
+ if ((id == 'Left' || id == 'Right') && this.expandedItem) {
+ var cs = this.ownerDocument.defaultView.getComputedStyle(this);
+ var rtl = cs.direction == 'rtl';
+ if ((!rtl && id == 'Left') || (rtl && id == 'Right'))
+ this.expandedItem.selectedIndex--;
+ else
+ this.expandedItem.selectedIndex++;
+ this.scrollIndexIntoView(this.expandedItem.listIndex);
+ // Prevent the page itself from scrolling.
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * Called on selection model selection changes.
+ * @param {Event} ce The selection change event.
+ * @private
+ */
+ cookieSelectionChange_: function(ce) {
+ ce.changes.forEach(function(change) {
+ var listItem = this.getListItemByIndex(change.index);
+ if (listItem) {
+ if (!change.selected) {
+ // We set a timeout here, rather than setting the item unexpanded
+ // immediately, so that if another item gets set expanded right
+ // away, it will be expanded before this item is unexpanded. It
+ // will notice that, and unexpand this item in sync with its own
+ // expansion. Later, this callback will end up having no effect.
+ window.setTimeout(function() {
+ if (!listItem.selected || !listItem.lead)
+ listItem.expanded = false;
+ }, 0);
+ } else if (listItem.lead) {
+ listItem.expanded = true;
+ }
+ }
+ }, this);
+ },
+
+ /**
+ * Called on selection model lead changes.
+ * @param {Event} pe The lead change event.
+ * @private
+ */
+ cookieLeadChange_: function(pe) {
+ if (pe.oldValue != -1) {
+ var listItem = this.getListItemByIndex(pe.oldValue);
+ if (listItem) {
+ // See cookieSelectionChange_ above for why we use a timeout here.
+ window.setTimeout(function() {
+ if (!listItem.lead || !listItem.selected)
+ listItem.expanded = false;
+ }, 0);
+ }
+ }
+ if (pe.newValue != -1) {
+ var listItem = this.getListItemByIndex(pe.newValue);
+ if (listItem && listItem.selected)
+ listItem.expanded = true;
+ }
+ },
+
+ /**
+ * The currently expanded item. Used by IntentsListItem above.
+ * @type {?IntentsListItem}
+ */
+ expandedItem: null,
+
+ // from cr.ui.List
+ /** @inheritDoc */
+ createItem: function(data) {
+ // We use the cached expanded item in order to allow it to maintain some
+ // state (like its fixed height, and which bubble is selected).
+ if (this.expandedItem && this.expandedItem.origin == data)
+ return this.expandedItem;
+ return new IntentsListItem(data, this);
+ },
+
+ // from options.DeletableItemList
+ /** @inheritDoc */
+ deleteItemAtIndex: function(index) {
+ var item = this.data_[index];
+ if (item) {
+ var pathId = item.pathId;
+ if (pathId)
+ chrome.send('removeIntent', [pathId]);
+ }
+ },
+
+ /**
+ * Insert an intents tree node at the given index.
+ * Both IntentsList and IntentsTreeNode implement this API.
+ * @param {Object} data The data object for the node to add.
+ * @param {number} index The index at which to insert the node.
+ */
+ insertAt: function(data, index) {
+ this.dataModel.splice(index, 0, new IntentsTreeNode(data));
+ },
+
+ /**
+ * Remove an intents tree node from the given index.
+ * Both IntentsList and IntentsTreeNode implement this API.
+ * @param {number} index The index of the tree node to remove.
+ */
+ remove: function(index) {
+ if (index < this.data_.length)
+ this.dataModel.splice(index, 1);
+ },
+
+ /**
+ * Clears the list.
+ * Both IntentsList and IntentsTreeNode implement this API.
+ * It is used by IntentsList.loadChildren().
+ */
+ clear: function() {
+ parentLookup = {};
+ this.data_ = [];
+ this.dataModel = new ArrayDataModel(this.data_);
+ this.redraw();
+ },
+
+ /**
+ * Add tree nodes by given parent.
+ * Note: this method will be O(n^2) in the general case. Use it only to
+ * populate an empty parent or to insert single nodes to avoid this.
+ * @param {Object} parent The parent node.
+ * @param {number} start Start index of where to insert nodes.
+ * @param {Array} nodesData Nodes data array.
+ * @private
+ */
+ addByParent_: function(parent, start, nodesData) {
+ if (!parent)
+ return;
+
+ parent.startBatchUpdates();
+ for (var i = 0; i < nodesData.length; ++i)
+ parent.insertAt(nodesData[i], start + i);
+ parent.endBatchUpdates();
+
+ cr.dispatchSimpleEvent(this, 'change');
+ },
+
+ /**
+ * Add tree nodes by parent id.
+ * This is used by intents_view.js.
+ * Note: this method will be O(n^2) in the general case. Use it only to
+ * populate an empty parent or to insert single nodes to avoid this.
+ * @param {string} parentId Id of the parent node.
+ * @param {number} start Start index of where to insert nodes.
+ * @param {Array} nodesData Nodes data array.
+ */
+ addByParentId: function(parentId, start, nodesData) {
+ var parent = parentId ? parentLookup[parentId] : this;
+ this.addByParent_(parent, start, nodesData);
+ },
+
+ /**
+ * Removes tree nodes by parent id.
+ * This is used by intents_view.js.
+ * @param {string} parentId Id of the parent node.
+ * @param {number} start Start index of nodes to remove.
+ * @param {number} count Number of nodes to remove.
+ */
+ removeByParentId: function(parentId, start, count) {
+ var parent = parentId ? parentLookup[parentId] : this;
+ if (!parent)
+ return;
+
+ parent.startBatchUpdates();
+ while (count-- > 0)
+ parent.remove(start);
+ parent.endBatchUpdates();
+
+ cr.dispatchSimpleEvent(this, 'change');
+ },
+
+ /**
+ * Loads the immediate children of given parent node.
+ * This is used by intents_view.js.
+ * @param {string} parentId Id of the parent node.
+ * @param {Array} children The immediate children of parent node.
+ */
+ loadChildren: function(parentId, children) {
+ console.log('Loading intents view: ' +
+ parentId + ' ' + JSON.stringify(children));
+ if (parentId)
+ delete lookupRequests[parentId];
+ var parent = parentId ? parentLookup[parentId] : this;
+ if (!parent)
+ return;
+
+ parent.startBatchUpdates();
+ parent.clear();
+ this.addByParent_(parent, 0, children);
+ parent.endBatchUpdates();
+ },
+ };
+
+ return {
+ IntentsList: IntentsList
+ };
+});
diff --git a/chrome/browser/resources/options/intents_view.css b/chrome/browser/resources/options/intents_view.css
new file mode 100644
index 0000000..84c274d
--- /dev/null
+++ b/chrome/browser/resources/options/intents_view.css
@@ -0,0 +1,181 @@
+/*
+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.
+*/
+
+/* Styles for the intents list elements in intents_view.html. */
+
+#intents-column-headers {
+ position: relative;
+ width: 100%;
+}
+
+#intents-column-headers h3 {
+ font-size: 105%;
+ font-weight: bold;
+ margin: 10px 0;
+}
+
+/* Notice the width and padding for these columns match up with those below. */
+#intents-site-column {
+ display: inline-block;
+ font-weight: bold;
+ width: 11em;
+}
+
+#intents-data-column {
+ -webkit-padding-start: 7px;
+ display: inline-block;
+ font-weight: bold;
+}
+
+#intents-list {
+ border: 1px solid #d9d9d9;
+ margin: 0;
+}
+
+/* Enable animating the height of items. */
+list.intents-list .deletable-item {
+ -webkit-transition: height .15s ease-in-out;
+}
+
+/* Disable webkit-box display. */
+list.intents-list .deletable-item > :first-child {
+ display: block;
+}
+
+/* Force the X for deleting an origin to stay at the top. */
+list.intents-list > .deletable-item > .close-button {
+ position: absolute;
+ right: 2px;
+ top: 8px;
+}
+
+html[dir=rtl] list.intents-list > .deletable-item > .close-button {
+ left: 2px;
+ right: auto;
+}
+
+/* Styles for the site (aka origin) and its summary. */
+
+.intents-site {
+ /* Notice that the width, margin, and padding match up with those above. */
+ -webkit-margin-end: 2px;
+ -webkit-padding-start: 5px;
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 11em;
+}
+
+list.intents-list > .deletable-item[selected] .intents-site {
+ -webkit-user-select: text;
+}
+
+.intents-data {
+ display: inline-block;
+}
+
+list.intents-list > .deletable-item[selected] .intents-data {
+ -webkit-user-select: text;
+}
+
+.intents-items {
+ /* Notice that the margin and padding match up with those above. */
+ -webkit-margin-start: 11em;
+ -webkit-padding-start: 7px;
+ -webkit-transition: .15s ease-in-out;
+ display: table;
+ height: 0;
+ opacity: 0;
+ /* Make the intents items wrap correctly. */
+ white-space: normal;
+}
+
+.measure-items .intents-items {
+ -webkit-transition: none;
+ height: auto;
+ visibility: hidden;
+}
+
+.show-items .intents-items {
+ opacity: 1;
+}
+
+.intents-items .intents-item {
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: start;
+ background: #e0e9f5;
+ border: 1px solid #8392ae;
+ display: table-row;
+ font-size: 85%;
+ height: auto;
+ margin: 2px 4px 2px 0;
+ overflow: hidden;
+ padding: 0 3px;
+ text-align: center;
+ text-overflow: ellipsis;
+}
+
+.intents-item .intents-item-action {
+ display: table-cell;
+ padding: 2px 5px;
+}
+
+.intents-item .intents-item-types {
+ display: table-cell;
+ padding: 2px 5px;
+ overflow: hidden;
+}
+
+.intents-item .intents-item-url {
+ display: table-cell;
+ padding: 2px 5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.intents-items .intents-item:hover {
+ background: #eef3f9;
+ border-color: #647187;
+}
+
+.intents-items .intents-item[selected] {
+ background: #f5f8f8;
+ border-color: #b2b2b2;
+}
+
+.intents-items .intents-item[selected]:hover {
+ background: #f5f8f8;
+ border-color: #647187;
+}
+
+/* Styles for the intents details box. */
+
+.intents-details {
+ background: #f5f8f8;
+ border-radius: 5px;
+ border: 1px solid #b2b2b2;
+ margin-top: 2px;
+ padding: 5px;
+}
+
+list.intents-list > .deletable-item[selected] .intents-details {
+ -webkit-user-select: text;
+}
+
+.intents-details-table {
+ table-layout: fixed;
+ width: 100%;
+}
+
+.intents-details-label {
+ vertical-align: top;
+ white-space: pre;
+ width: 10em;
+}
+
+.intents-details-value {
+ word-wrap: break-word;
+}
diff --git a/chrome/browser/resources/options/intents_view.html b/chrome/browser/resources/options/intents_view.html
new file mode 100644
index 0000000..6803c23
--- /dev/null
+++ b/chrome/browser/resources/options/intents_view.html
@@ -0,0 +1,12 @@
+<div id="intents-view-page" class="page" hidden>
+ <h1 i18n-content="intentsViewPage"></h1>
+ <div id="intents-column-headers">
+ <div id="intents-site-column">
+ <h3 i18n-content="intentsDomain"></h3>
+ </div>
+ <div id="intents-data-column">
+ <h3 i18n-content="intentsServiceData"></h3>
+ </div>
+ </div>
+ <list id="intents-list"></list>
+</div>
diff --git a/chrome/browser/resources/options/intents_view.js b/chrome/browser/resources/options/intents_view.js
new file mode 100644
index 0000000..afc7d68
--- /dev/null
+++ b/chrome/browser/resources/options/intents_view.js
@@ -0,0 +1,83 @@
+// 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.
+
+cr.define('options', function() {
+ var OptionsPage = options.OptionsPage;
+
+ /////////////////////////////////////////////////////////////////////////////
+ // IntentsView class:
+
+ /**
+ * Encapsulated handling of the Intents data page.
+ * @constructor
+ */
+ function IntentsView(model) {
+ OptionsPage.call(this, 'intents',
+ templateData.intentsViewPageTabTitle,
+ 'intents-view-page');
+ }
+
+ cr.addSingletonGetter(IntentsView);
+
+ IntentsView.prototype = {
+ __proto__: OptionsPage.prototype,
+
+ initializePage: function() {
+ OptionsPage.prototype.initializePage.call(this);
+
+ var intentsList = $('intents-list');
+ options.IntentsList.decorate(intentsList);
+ window.addEventListener('resize', this.handleResize_.bind(this));
+
+ this.addEventListener('visibleChange', this.handleVisibleChange_);
+ },
+
+ initialized_: false,
+
+ /**
+ * Handler for OptionsPage's visible property change event.
+ * @param {Event} e Property change event.
+ * @private
+ */
+ handleVisibleChange_: function(e) {
+ if (!this.visible)
+ return;
+
+ // Resize the intents list whenever the options page becomes visible.
+ this.handleResize_(null);
+ if (!this.initialized_) {
+ this.initialized_ = true;
+ chrome.send('loadIntents');
+ } else {
+ $('intents-list').redraw();
+ }
+ },
+
+ /**
+ * Handler for when the window changes size. Resizes the intents list to
+ * match the window height.
+ * @param {?Event} e Window resize event, or null if called directly.
+ * @private
+ */
+ handleResize_: function(e) {
+ if (!this.visible)
+ return;
+ var intentsList = $('intents-list');
+ // 25 pixels from the window bottom seems like a visually pleasing amount.
+ var height = window.innerHeight - intentsList.offsetTop - 25;
+ intentsList.style.height = height + 'px';
+ },
+ };
+
+ // IntentsViewHandler callbacks.
+ IntentsView.loadChildren = function(args) {
+ $('intents-list').loadChildren(args[0], args[1]);
+ };
+
+ // Export
+ return {
+ IntentsView: IntentsView
+ };
+
+});
diff --git a/chrome/browser/resources/options/options.html b/chrome/browser/resources/options/options.html
index 3fb7c74..de3754c 100644
--- a/chrome/browser/resources/options/options.html
+++ b/chrome/browser/resources/options/options.html
@@ -29,6 +29,9 @@
<link rel="stylesheet" href="handler_options.css">
</if>
<link rel="stylesheet" href="import_data_overlay.css">
+<if expr="pp_ifdef('enable_web_intents')">
+ <link rel="stylesheet" href="intents_view.css">
+</if>
<link rel="stylesheet" href="language_options.css">
<link rel="stylesheet" href="manage_profile_overlay.css">
<link rel="stylesheet" href="password_manager.css">
@@ -164,6 +167,9 @@
<if expr="pp_ifdef('enable_register_protocol_handler')">
<include src="handler_options.html">
</if>
+ <if expr="pp_ifdef('enable_web_intents')">
+ <include src="intents_view.html">
+ </if>
<include src="content_settings_exceptions_area.html">
</div>
</div>
diff --git a/chrome/browser/resources/options/options.js b/chrome/browser/resources/options/options.js
index de55f64..3e5c41b 100644
--- a/chrome/browser/resources/options/options.js
+++ b/chrome/browser/resources/options/options.js
@@ -17,6 +17,7 @@ var CookiesView = options.CookiesView;
var FontSettings = options.FontSettings;
var HandlerOptions = options.HandlerOptions;
var ImportDataOverlay = options.ImportDataOverlay;
+var IntentsView = options.IntentsView;
var InstantConfirmOverlay = options.InstantConfirmOverlay;
var LanguageOptions = options.LanguageOptions;
var OptionsPage = options.OptionsPage;
@@ -120,6 +121,11 @@ function load() {
ContentSettings.getInstance(),
[$('manage-handlers-button')]);
}
+ if (IntentsView && $('manage-intents-button')) {
+ OptionsPage.registerSubPage(IntentsView.getInstance(),
+ ContentSettings.getInstance(),
+ [$('manage-intents-button')]);
+ }
OptionsPage.registerSubPage(FontSettings.getInstance(),
AdvancedOptions.getInstance(),
[$('fontSettingsCustomizeFontsButton')]);
diff --git a/chrome/browser/resources/options/options_bundle.js b/chrome/browser/resources/options/options_bundle.js
index a181de9..0b58a74 100644
--- a/chrome/browser/resources/options/options_bundle.js
+++ b/chrome/browser/resources/options/options_bundle.js
@@ -68,6 +68,10 @@
</if>
<include src="import_data_overlay.js"></include>
<include src="instant_confirm_overlay.js"></include>
+<if expr="pp_ifdef('enable_web_intents')">
+ <include src="intents_list.js"></include>
+ <include src="intents_view.js"></include>
+</if>
<include src="language_add_language_overlay.js"></include>
<include src="language_list.js"></include>
<include src="language_options.js"></include>
diff --git a/chrome/browser/ui/intents/intents_model.cc b/chrome/browser/ui/intents/intents_model.cc
new file mode 100644
index 0000000..919dbe3
--- /dev/null
+++ b/chrome/browser/ui/intents/intents_model.cc
@@ -0,0 +1,162 @@
+// 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.
+
+#include "chrome/browser/ui/intents/intents_model.h"
+#include "base/string_split.h"
+#include "base/string_util.h"
+#include "base/stringprintf.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/intents/web_intents_registry.h"
+
+IntentsTreeNode::IntentsTreeNode()
+ : ui::TreeNode<IntentsTreeNode>(string16()),
+ type_(TYPE_ROOT) {}
+
+IntentsTreeNode::IntentsTreeNode(const string16& title)
+ : ui::TreeNode<IntentsTreeNode>(title),
+ type_(TYPE_ORIGIN) {}
+
+IntentsTreeNode::~IntentsTreeNode() {}
+
+ServiceTreeNode::ServiceTreeNode(const string16& title)
+ : IntentsTreeNode(title, IntentsTreeNode::TYPE_SERVICE),
+ blocked_(false),
+ disabled_(false) {}
+
+ServiceTreeNode::~ServiceTreeNode() {}
+
+IntentsModel::IntentsModel(WebIntentsRegistry* intents_registry)
+ : ui::TreeNodeModel<IntentsTreeNode>(new IntentsTreeNode()),
+ intents_registry_(intents_registry),
+ batch_update_(0) {
+ LoadModel();
+}
+
+IntentsModel::~IntentsModel() {}
+
+void IntentsModel::AddIntentsTreeObserver(Observer* observer) {
+ intents_observer_list_.AddObserver(observer);
+ // Call super so that TreeNodeModel can notify, too.
+ ui::TreeNodeModel<IntentsTreeNode>::AddObserver(observer);
+}
+
+void IntentsModel::RemoveIntentsTreeObserver(Observer* observer) {
+ intents_observer_list_.RemoveObserver(observer);
+ // Call super so that TreeNodeModel doesn't have dead pointers.
+ ui::TreeNodeModel<IntentsTreeNode>::RemoveObserver(observer);
+}
+
+string16 IntentsModel::GetTreeNodeId(IntentsTreeNode* node) {
+ if (node->Type() == IntentsTreeNode::TYPE_ORIGIN)
+ return node->GetTitle();
+
+ // TODO(gbillock): handle TYPE_SERVICE when/if we ever want to do
+ // specific managing of them.
+
+ return string16();
+}
+
+IntentsTreeNode* IntentsModel::GetTreeNode(std::string path_id) {
+ if (path_id.empty())
+ return GetRoot();
+
+ std::vector<std::string> node_ids;
+ base::SplitString(path_id, ',', &node_ids);
+
+ for (int i = 0; i < GetRoot()->child_count(); ++i) {
+ IntentsTreeNode* node = GetRoot()->GetChild(i);
+ if (UTF16ToUTF8(node->GetTitle()) == node_ids[0]) {
+ if (node_ids.size() == 1)
+ return node;
+ }
+ }
+
+ // TODO: support service nodes?
+ return NULL;
+}
+
+void IntentsModel::GetChildNodeList(IntentsTreeNode* parent,
+ int start, int count,
+ base::ListValue* nodes) {
+ for (int i = 0; i < count; ++i) {
+ base::DictionaryValue* dict = new base::DictionaryValue;
+ IntentsTreeNode* child = parent->GetChild(start + i);
+ GetIntentsTreeNodeDictionary(*child, dict);
+ nodes->Append(dict);
+ }
+}
+
+void IntentsModel::GetIntentsTreeNodeDictionary(const IntentsTreeNode& node,
+ base::DictionaryValue* dict) {
+ if (node.Type() == IntentsTreeNode::TYPE_ROOT) {
+ return;
+ }
+
+ if (node.Type() == IntentsTreeNode::TYPE_ORIGIN) {
+ dict->SetString("site", node.GetTitle());
+ dict->SetBoolean("hasChildren", node.child_count() > 0);
+ return;
+ }
+
+ if (node.Type() == IntentsTreeNode::TYPE_SERVICE) {
+ const ServiceTreeNode* snode = static_cast<const ServiceTreeNode*>(&node);
+ dict->SetString("site", snode->GetTitle());
+ dict->SetString("name", snode->ServiceName());
+ dict->SetString("url", snode->ServiceUrl());
+ dict->SetString("icon", snode->IconUrl());
+ dict->SetString("action", snode->Action());
+ dict->Set("types", snode->Types().DeepCopy());
+ dict->SetBoolean("blocked", snode->IsBlocked());
+ dict->SetBoolean("disabled", snode->IsDisabled());
+ return;
+ }
+}
+
+void IntentsModel::LoadModel() {
+ NotifyObserverBeginBatch();
+ intents_registry_->GetAllIntentProviders(this);
+}
+
+void IntentsModel::OnIntentsQueryDone(
+ WebIntentsRegistry::QueryID query_id,
+ const std::vector<WebIntentData>& intents) {
+ for (size_t i = 0; i < intents.size(); ++i) {
+ // Eventually do some awesome sorting, grouping, clustering stuff here.
+ // For now, just stick it in the model flat.
+ IntentsTreeNode* n = new IntentsTreeNode(ASCIIToUTF16(
+ intents[i].service_url.host()));
+ ServiceTreeNode* ns = new ServiceTreeNode(ASCIIToUTF16(
+ intents[i].service_url.host()));
+ ns->SetServiceName(intents[i].title);
+ ns->SetServiceUrl(ASCIIToUTF16(intents[i].service_url.spec()));
+ GURL icon_url = intents[i].service_url.GetOrigin().Resolve("/favicon.ico");
+ ns->SetIconUrl(ASCIIToUTF16(icon_url.spec()));
+ ns->SetAction(intents[i].action);
+ ns->AddType(intents[i].type);
+ // Won't generate a notification. OK for now as the next line will.
+ n->Add(ns, 0);
+ Add(GetRoot(), n, GetRoot()->child_count());
+ }
+
+ NotifyObserverEndBatch();
+}
+
+void IntentsModel::NotifyObserverBeginBatch() {
+ // Only notify the model once if we're batching in a nested manner.
+ if (batch_update_++ == 0) {
+ FOR_EACH_OBSERVER(Observer,
+ intents_observer_list_,
+ TreeModelBeginBatch(this));
+ }
+}
+
+void IntentsModel::NotifyObserverEndBatch() {
+ // Only notify the observers if this is the outermost call to EndBatch() if
+ // called in a nested manner.
+ if (--batch_update_ == 0) {
+ FOR_EACH_OBSERVER(Observer,
+ intents_observer_list_,
+ TreeModelEndBatch(this));
+ }
+}
diff --git a/chrome/browser/ui/intents/intents_model.h b/chrome/browser/ui/intents/intents_model.h
new file mode 100644
index 0000000..14e8bbf
--- /dev/null
+++ b/chrome/browser/ui/intents/intents_model.h
@@ -0,0 +1,128 @@
+// 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.
+
+#ifndef CHROME_BROWSER_UI_INTENTS_INTENTS_MODEL_H_
+#define CHROME_BROWSER_UI_INTENTS_INTENTS_MODEL_H_
+#pragma once
+
+#include "base/values.h"
+#include "chrome/browser/intents/web_intents_registry.h"
+#include "ui/base/models/tree_node_model.h"
+
+class WebIntentsRegistry;
+
+// The tree structure is a TYPE_ROOT node with title="",
+// children are TYPE_ORIGIN nodes with title=origin, whose
+// children are TYPE_SERVICE nodes with title=origin, and
+// will be of type ServiceTreeNode with data on individual
+// services.
+class IntentsTreeNode : public ui::TreeNode<IntentsTreeNode> {
+ public:
+ IntentsTreeNode();
+ explicit IntentsTreeNode(const string16& title);
+
+ virtual ~IntentsTreeNode();
+
+ enum NodeType {
+ TYPE_ROOT,
+ TYPE_ORIGIN,
+ TYPE_SERVICE,
+ };
+
+ NodeType Type() const { return type_; }
+
+ protected:
+ IntentsTreeNode(const string16& title, NodeType type)
+ : ui::TreeNode<IntentsTreeNode>(title),
+ type_(type) {}
+
+ private:
+ NodeType type_;
+};
+
+// Tree node representing particular services presented by an origin.
+class ServiceTreeNode : public IntentsTreeNode {
+ public:
+ explicit ServiceTreeNode(const string16& title);
+ virtual ~ServiceTreeNode();
+
+ const string16& ServiceName() const { return service_name_; }
+ const string16& ServiceUrl() const { return service_url_; }
+ const string16& IconUrl() const { return icon_url_; }
+ const string16& Action() const { return action_; }
+ const base::ListValue& Types() const { return types_; }
+ bool IsBlocked() const { return blocked_; }
+ bool IsDisabled() const { return disabled_; }
+
+ void SetServiceName(string16 name) { service_name_ = name; }
+ void SetServiceUrl(string16 url) { service_url_ = url; }
+ void SetIconUrl(string16 url) { icon_url_ = url; }
+ void SetAction(string16 action) { action_ = action; }
+ void AddType(string16 type) { types_.Append(Value::CreateStringValue(type)); }
+ void SetBlocked(bool blocked) { blocked_ = blocked; }
+ void SetDisabled(bool disabled) { disabled_ = disabled; }
+
+ private:
+ string16 service_name_;
+ string16 icon_url_;
+ string16 service_url_;
+ string16 action_;
+ base::ListValue types_;
+
+ // TODO(gbillock): these are kind of a placeholder for exceptions data.
+ bool blocked_;
+ bool disabled_;
+};
+
+// UI-backing tree model of the data in the WebIntentsRegistry.
+class IntentsModel : public ui::TreeNodeModel<IntentsTreeNode>,
+ public WebIntentsRegistry::Consumer {
+ public:
+ // Because nodes are fetched in a background thread, they are not
+ // present at the time the Model is created. The Model then notifies its
+ // observers for every item added.
+ class Observer : public ui::TreeModelObserver {
+ public:
+ virtual void TreeModelBeginBatch(IntentsModel* model) {}
+ virtual void TreeModelEndBatch(IntentsModel* model) {}
+ };
+
+ explicit IntentsModel(WebIntentsRegistry* intents_registry);
+ virtual ~IntentsModel();
+
+ void AddIntentsTreeObserver(Observer* observer);
+ void RemoveIntentsTreeObserver(Observer* observer);
+
+ string16 GetTreeNodeId(IntentsTreeNode* node);
+ IntentsTreeNode* GetTreeNode(std::string path_id);
+ void GetChildNodeList(IntentsTreeNode* parent, int start, int count,
+ base::ListValue* nodes);
+ void GetIntentsTreeNodeDictionary(const IntentsTreeNode& node,
+ base::DictionaryValue* dict);
+
+ virtual void OnIntentsQueryDone(
+ WebIntentsRegistry::QueryID query_id,
+ const std::vector<WebIntentData>& intents) OVERRIDE;
+
+ private:
+ // Loads the data model from the WebIntentsRegistry.
+ // TODO(gbillock): need an observer on that to absorb async updates?
+ void LoadModel();
+
+ // Do batch-specific notifies for updates coming from the LoadModel.
+ void NotifyObserverBeginBatch();
+ void NotifyObserverEndBatch();
+
+ // The backing registry. Weak pointer.
+ WebIntentsRegistry* intents_registry_;
+
+ // Separate list of observers that'll get batch updates.
+ ObserverList<Observer> intents_observer_list_;
+
+ // Batch update nesting level. Incremented to indicate that we're in
+ // the middle of a batch update.
+ int batch_update_;
+};
+
+#endif // CHROME_BROWSER_UI_INTENTS_INTENTS_MODEL_H_
diff --git a/chrome/browser/ui/intents/intents_model_unittest.cc b/chrome/browser/ui/intents/intents_model_unittest.cc
new file mode 100644
index 0000000..175e546
--- /dev/null
+++ b/chrome/browser/ui/intents/intents_model_unittest.cc
@@ -0,0 +1,185 @@
+// 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.
+
+#include "base/file_util.h"
+#include "base/scoped_temp_dir.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/values.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/intents/web_intents_registry.h"
+#include "chrome/browser/webdata/web_data_service.h"
+#include "chrome/test/base/testing_browser_process_test.h"
+#include "chrome/browser/ui/intents/intents_model.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/models/tree_node_model.h"
+
+class IntentsModelTest : public TestingBrowserProcessTest {
+ public:
+ IntentsModelTest()
+ : ui_thread_(BrowserThread::UI, &message_loop_),
+ db_thread_(BrowserThread::DB) {}
+
+ protected:
+ virtual void SetUp() {
+ db_thread_.Start();
+ wds_ = new WebDataService();
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+ wds_->Init(temp_dir_.path());
+
+ registry_.Initialize(wds_);
+ }
+
+ virtual void TearDown() {
+ if (wds_.get())
+ wds_->Shutdown();
+
+ db_thread_.Stop();
+ MessageLoop::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask);
+ MessageLoop::current()->Run();
+ }
+
+ void LoadRegistry() {
+ {
+ WebIntentData provider;
+ provider.service_url = GURL("http://www.google.com/share");
+ provider.action = ASCIIToUTF16("SHARE");
+ provider.type = ASCIIToUTF16("text/url");
+ provider.title = ASCIIToUTF16("Google");
+ registry_.RegisterIntentProvider(provider);
+ }
+ {
+ WebIntentData provider;
+ provider.service_url = GURL("http://picasaweb.google.com/share");
+ provider.action = ASCIIToUTF16("EDIT");
+ provider.type = ASCIIToUTF16("image/*");
+ provider.title = ASCIIToUTF16("Picasa");
+ registry_.RegisterIntentProvider(provider);
+ }
+ {
+ WebIntentData provider;
+ provider.service_url = GURL("http://www.digg.com/share");
+ provider.action = ASCIIToUTF16("SHARE");
+ provider.type = ASCIIToUTF16("text/url");
+ provider.title = ASCIIToUTF16("Digg");
+ registry_.RegisterIntentProvider(provider);
+ }
+ }
+
+ MessageLoopForUI message_loop_;
+ BrowserThread ui_thread_;
+ BrowserThread db_thread_;
+ scoped_refptr<WebDataService> wds_;
+ WebIntentsRegistry registry_;
+ ScopedTempDir temp_dir_;
+};
+
+class WaitingIntentsObserver : public IntentsModel::Observer {
+ public:
+ WaitingIntentsObserver() : event_(true, false), added_(0) {}
+
+ virtual void TreeModelBeginBatch(IntentsModel* model) {}
+
+ virtual void TreeModelEndBatch(IntentsModel* model) {
+ event_.Signal();
+ MessageLoop::current()->Quit();
+ }
+
+ virtual void TreeNodesAdded(ui::TreeModel* model,
+ ui::TreeModelNode* parent,
+ int start,
+ int count) {
+ added_++;
+ }
+
+ virtual void TreeNodesRemoved(ui::TreeModel* model,
+ ui::TreeModelNode* node,
+ int start,
+ int count) {
+ }
+
+ virtual void TreeNodeChanged(ui::TreeModel* model, ui::TreeModelNode* node) {
+ }
+
+ void Wait() {
+ MessageLoop::current()->Run();
+ event_.Wait();
+ LOG(INFO) << "DONE!";
+ }
+
+ base::WaitableEvent event_;
+ int added_;
+};
+
+TEST_F(IntentsModelTest, NodeIDs) {
+ LoadRegistry();
+ WaitingIntentsObserver obs;
+ IntentsModel intents_model(&registry_);
+ intents_model.AddIntentsTreeObserver(&obs);
+ obs.Wait();
+
+ IntentsTreeNode* n1 = new IntentsTreeNode(ASCIIToUTF16("origin"));
+ intents_model.Add(intents_model.GetRoot(), n1,
+ intents_model.GetRoot()->child_count());
+ EXPECT_EQ(ASCIIToUTF16("origin"), intents_model.GetTreeNodeId(n1));
+
+ IntentsTreeNode* ncheck = intents_model.GetTreeNode("origin");
+ EXPECT_EQ(ncheck, n1);
+
+ base::ListValue nodes;
+ intents_model.GetChildNodeList(
+ intents_model.GetTreeNode("www.google.com"), 0, 1, &nodes);
+ EXPECT_EQ(static_cast<size_t>(1), nodes.GetSize());
+ base::DictionaryValue* dict;
+ EXPECT_TRUE(nodes.GetDictionary(0, &dict));
+
+ std::string val;
+ EXPECT_TRUE(dict->GetString("site", &val));
+ EXPECT_EQ("www.google.com", val);
+ EXPECT_TRUE(dict->GetString("name", &val));
+ EXPECT_EQ("Google", val);
+ EXPECT_TRUE(dict->GetString("url", &val));
+ EXPECT_EQ("http://www.google.com/share", val);
+ EXPECT_TRUE(dict->GetString("icon", &val));
+ EXPECT_EQ("http://www.google.com/favicon.ico", val);
+ base::ListValue* types_list;
+ EXPECT_TRUE(dict->GetList("types", &types_list));
+ EXPECT_EQ(static_cast<size_t>(1), types_list->GetSize());
+ EXPECT_TRUE(types_list->GetString(0, &val));
+ EXPECT_EQ("text/url", val);
+ bool bval;
+ EXPECT_TRUE(dict->GetBoolean("blocked", &bval));
+ EXPECT_FALSE(bval);
+ EXPECT_TRUE(dict->GetBoolean("disabled", &bval));
+ EXPECT_FALSE(bval);
+}
+
+TEST_F(IntentsModelTest, LoadFromWebData) {
+ LoadRegistry();
+ WaitingIntentsObserver obs;
+ IntentsModel intents_model(&registry_);
+ intents_model.AddIntentsTreeObserver(&obs);
+ obs.Wait();
+ EXPECT_EQ(3, obs.added_);
+
+ IntentsTreeNode* node = intents_model.GetTreeNode("www.google.com");
+ ASSERT_NE(static_cast<IntentsTreeNode*>(NULL), node);
+ EXPECT_EQ(IntentsTreeNode::TYPE_ORIGIN, node->Type());
+ EXPECT_EQ(ASCIIToUTF16("www.google.com"), node->GetTitle());
+ EXPECT_EQ(1, node->child_count());
+ node = node->GetChild(0);
+ ASSERT_EQ(IntentsTreeNode::TYPE_SERVICE, node->Type());
+ ServiceTreeNode* snode = static_cast<ServiceTreeNode*>(node);
+ EXPECT_EQ(ASCIIToUTF16("Google"), snode->ServiceName());
+ EXPECT_EQ(ASCIIToUTF16("SHARE"), snode->Action());
+ EXPECT_EQ(ASCIIToUTF16("http://www.google.com/share"), snode->ServiceUrl());
+ EXPECT_EQ(static_cast<size_t>(1), snode->Types().GetSize());
+ string16 stype;
+ ASSERT_TRUE(snode->Types().GetString(0, &stype));
+ EXPECT_EQ(ASCIIToUTF16("text/url"), stype);
+
+ node = intents_model.GetTreeNode("www.digg.com");
+ ASSERT_NE(static_cast<IntentsTreeNode*>(NULL), node);
+ EXPECT_EQ(IntentsTreeNode::TYPE_ORIGIN, node->Type());
+ EXPECT_EQ(ASCIIToUTF16("www.digg.com"), node->GetTitle());
+}
diff --git a/chrome/browser/ui/webui/options/intents_settings_handler.cc b/chrome/browser/ui/webui/options/intents_settings_handler.cc
new file mode 100644
index 0000000..42f4868
--- /dev/null
+++ b/chrome/browser/ui/webui/options/intents_settings_handler.cc
@@ -0,0 +1,159 @@
+// 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.
+
+#include "chrome/browser/ui/webui/options/intents_settings_handler.h"
+
+#include "base/utf_string_conversions.h"
+#include "base/values.h"
+#include "chrome/browser/browsing_data_appcache_helper.h"
+#include "chrome/browser/browsing_data_database_helper.h"
+#include "chrome/browser/browsing_data_file_system_helper.h"
+#include "chrome/browser/browsing_data_indexed_db_helper.h"
+#include "chrome/browser/browsing_data_local_storage_helper.h"
+#include "chrome/browser/intents/web_intents_registry.h"
+#include "chrome/browser/intents/web_intents_registry_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/webdata/web_data_service.h"
+#include "content/browser/webui/web_ui.h"
+#include "grit/generated_resources.h"
+#include "net/url_request/url_request_context_getter.h"
+#include "ui/base/l10n/l10n_util.h"
+
+IntentsSettingsHandler::IntentsSettingsHandler() : batch_update_(false) {
+}
+
+IntentsSettingsHandler::~IntentsSettingsHandler() {
+}
+
+void IntentsSettingsHandler::GetLocalizedValues(
+ DictionaryValue* localized_strings) {
+ DCHECK(localized_strings);
+
+ static OptionsStringResource resources[] = {
+ { "intentsDomain", IDS_INTENTS_DOMAIN_COLUMN_HEADER },
+ { "intentsServiceData", IDS_INTENTS_SERVICE_DATA_COLUMN_HEADER },
+ { "manageIntents", IDS_INTENTS_MANAGE_BUTTON },
+ { "removeIntent", IDS_INTENTS_REMOVE_INTENT_BUTTON },
+ };
+
+ RegisterStrings(localized_strings, resources, arraysize(resources));
+ RegisterTitle(localized_strings, "intentsViewPage",
+ IDS_INTENTS_MANAGER_WINDOW_TITLE);
+}
+
+void IntentsSettingsHandler::RegisterMessages() {
+ web_ui_->RegisterMessageCallback("removeIntent",
+ NewCallback(this, &IntentsSettingsHandler::RemoveIntent));
+ web_ui_->RegisterMessageCallback("loadIntents",
+ NewCallback(this, &IntentsSettingsHandler::LoadChildren));
+}
+
+void IntentsSettingsHandler::TreeNodesAdded(ui::TreeModel* model,
+ ui::TreeModelNode* parent,
+ int start,
+ int count) {
+ SendChildren(intents_tree_model_->GetRoot());
+}
+
+void IntentsSettingsHandler::TreeNodesRemoved(ui::TreeModel* model,
+ ui::TreeModelNode* parent,
+ int start,
+ int count) {
+ SendChildren(intents_tree_model_->GetRoot());
+}
+
+void IntentsSettingsHandler::TreeModelBeginBatch(IntentsModel* model) {
+ batch_update_ = true;
+}
+
+void IntentsSettingsHandler::TreeModelEndBatch(IntentsModel* model) {
+ batch_update_ = false;
+
+ SendChildren(intents_tree_model_->GetRoot());
+}
+
+void IntentsSettingsHandler::EnsureIntentsModelCreated() {
+ if (intents_tree_model_.get()) return;
+
+ Profile* profile = Profile::FromWebUI(web_ui_);
+ web_data_service_ = profile->GetWebDataService(Profile::EXPLICIT_ACCESS);
+ web_intents_registry_ = WebIntentsRegistryFactory::GetForProfile(profile);
+ web_intents_registry_->Initialize(web_data_service_.get());
+ intents_tree_model_.reset(new IntentsModel(web_intents_registry_));
+ intents_tree_model_->AddIntentsTreeObserver(this);
+}
+
+void IntentsSettingsHandler::RemoveIntent(const base::ListValue* args) {
+ std::string node_path;
+ if (!args->GetString(0, &node_path)) {
+ return;
+ }
+
+ EnsureIntentsModelCreated();
+
+ IntentsTreeNode* node = intents_tree_model_->GetTreeNode(node_path);
+ if (node->Type() == IntentsTreeNode::TYPE_ORIGIN) {
+ RemoveOrigin(node);
+ } else if (node->Type() == IntentsTreeNode::TYPE_SERVICE) {
+ ServiceTreeNode* snode = static_cast<ServiceTreeNode*>(node);
+ RemoveService(snode);
+ }
+}
+
+void IntentsSettingsHandler::RemoveOrigin(IntentsTreeNode* node) {
+ // TODO(gbillock): This is a known batch update. Worth optimizing?
+ while (node->child_count() > 0) {
+ IntentsTreeNode* cnode = node->GetChild(0);
+ CHECK(cnode->Type() == IntentsTreeNode::TYPE_SERVICE);
+ ServiceTreeNode* snode = static_cast<ServiceTreeNode*>(cnode);
+ RemoveService(snode);
+ }
+ delete intents_tree_model_->Remove(node->parent(), node);
+}
+
+void IntentsSettingsHandler::RemoveService(ServiceTreeNode* snode) {
+ WebIntentData provider;
+ provider.service_url = GURL(snode->ServiceUrl());
+ provider.action = snode->Action();
+ string16 stype;
+ if (snode->Types().GetString(0, &stype)) {
+ provider.type = stype; // Really need to iterate here.
+ }
+ provider.title = snode->ServiceName();
+ LOG(INFO) << "Removing service " << snode->ServiceName()
+ << " " << snode->ServiceUrl();
+ web_intents_registry_->UnregisterIntentProvider(provider);
+ delete intents_tree_model_->Remove(snode->parent(), snode);
+}
+
+void IntentsSettingsHandler::LoadChildren(const base::ListValue* args) {
+ EnsureIntentsModelCreated();
+
+ std::string node_path;
+ if (!args->GetString(0, &node_path)) {
+ SendChildren(intents_tree_model_->GetRoot());
+ return;
+ }
+
+ IntentsTreeNode* node = intents_tree_model_->GetTreeNode(node_path);
+ SendChildren(node);
+}
+
+void IntentsSettingsHandler::SendChildren(IntentsTreeNode* parent) {
+ // Early bailout during batch updates. We'll get one after the batch concludes
+ // with batch_update_ set false.
+ if (batch_update_) return;
+
+ ListValue* children = new ListValue;
+ intents_tree_model_->GetChildNodeList(parent, 0, parent->child_count(),
+ children);
+
+ ListValue args;
+ args.Append(parent == intents_tree_model_->GetRoot() ?
+ Value::CreateNullValue() :
+ Value::CreateStringValue(intents_tree_model_->GetTreeNodeId(parent)));
+ args.Append(children);
+
+ web_ui_->CallJavascriptFunction("IntentsView.loadChildren", args);
+}
diff --git a/chrome/browser/ui/webui/options/intents_settings_handler.h b/chrome/browser/ui/webui/options/intents_settings_handler.h
new file mode 100644
index 0000000..957284c
--- /dev/null
+++ b/chrome/browser/ui/webui/options/intents_settings_handler.h
@@ -0,0 +1,78 @@
+// 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.
+
+#ifndef CHROME_BROWSER_UI_WEBUI_OPTIONS_INTENTS_SETTINGS_HANDLER_H_
+#define CHROME_BROWSER_UI_WEBUI_OPTIONS_INTENTS_SETTINGS_HANDLER_H_
+#pragma once
+
+#include "base/compiler_specific.h"
+#include "base/memory/scoped_ptr.h"
+#include "chrome/browser/ui/intents/intents_model.h"
+#include "chrome/browser/ui/webui/options/options_ui.h"
+
+class WebDataService;
+class WebIntentsRegistry;
+
+// Manage setting up the backing data for the web intents options page.
+class IntentsSettingsHandler : public OptionsPageUIHandler,
+ public IntentsModel::Observer {
+ public:
+ IntentsSettingsHandler();
+ virtual ~IntentsSettingsHandler();
+
+ // OptionsPageUIHandler implementation.
+ virtual void GetLocalizedValues(base::DictionaryValue* localized_strings);
+ virtual void RegisterMessages();
+
+ // IntentsModel::Observer implementation.
+ virtual void TreeNodesAdded(ui::TreeModel* model,
+ ui::TreeModelNode* parent,
+ int start,
+ int count) OVERRIDE;
+ virtual void TreeNodesRemoved(ui::TreeModel* model,
+ ui::TreeModelNode* parent,
+ int start,
+ int count) OVERRIDE;
+ virtual void TreeNodeChanged(ui::TreeModel* model,
+ ui::TreeModelNode* node) OVERRIDE {}
+ virtual void TreeModelBeginBatch(IntentsModel* model) OVERRIDE;
+ virtual void TreeModelEndBatch(IntentsModel* model) OVERRIDE;
+
+ private:
+ // Creates the IntentsModel if neccessary.
+ void EnsureIntentsModelCreated();
+
+ // Updates search filter for cookies tree model.
+ void UpdateSearchResults(const base::ListValue* args);
+
+ // Remove all sites data.
+ void RemoveAll(const base::ListValue* args);
+
+ // Remove selected sites data.
+ void RemoveIntent(const base::ListValue* args);
+
+ // Helper functions for removals.
+ void RemoveOrigin(IntentsTreeNode* node);
+ void RemoveService(ServiceTreeNode* snode);
+
+ // Trigger for SendChildren to load the JS model.
+ void LoadChildren(const base::ListValue* args);
+
+ // Get children nodes data and pass it to 'IntentsView.loadChildren' to
+ // update the WebUI.
+ void SendChildren(IntentsTreeNode* parent);
+
+ scoped_refptr<WebDataService> web_data_service_;
+ WebIntentsRegistry* web_intents_registry_; // Weak pointer.
+
+ // Backing data model for the intents list.
+ scoped_ptr<IntentsModel> intents_tree_model_;
+
+ // Flag to indicate whether there is a batch update in progress.
+ bool batch_update_;
+
+ DISALLOW_COPY_AND_ASSIGN(IntentsSettingsHandler);
+};
+
+#endif // CHROME_BROWSER_UI_WEBUI_OPTIONS_INTENTS_SETTINGS_HANDLER_H_
diff --git a/chrome/browser/ui/webui/options/options_ui.cc b/chrome/browser/ui/webui/options/options_ui.cc
index 0847472..98e7d8b 100644
--- a/chrome/browser/ui/webui/options/options_ui.cc
+++ b/chrome/browser/ui/webui/options/options_ui.cc
@@ -28,6 +28,7 @@
#include "chrome/browser/ui/webui/options/core_options_handler.h"
#include "chrome/browser/ui/webui/options/font_settings_handler.h"
#include "chrome/browser/ui/webui/options/import_data_handler.h"
+#include "chrome/browser/ui/webui/options/intents_settings_handler.h"
#include "chrome/browser/ui/webui/options/language_options_handler.h"
#include "chrome/browser/ui/webui/options/manage_profile_handler.h"
#include "chrome/browser/ui/webui/options/options_sync_setup_handler.h"
@@ -210,6 +211,7 @@ OptionsUI::OptionsUI(TabContents* contents)
AddOptionsPageUIHandler(localized_strings, new ContentSettingsHandler());
AddOptionsPageUIHandler(localized_strings, new CookiesViewHandler());
AddOptionsPageUIHandler(localized_strings, new FontSettingsHandler());
+ AddOptionsPageUIHandler(localized_strings, new IntentsSettingsHandler());
#if defined(OS_CHROMEOS)
AddOptionsPageUIHandler(localized_strings,
new chromeos::CrosLanguageOptionsHandler());
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index f2403b2..c935047 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -2942,6 +2942,8 @@
'browser/ui/input_window_dialog.h',
'browser/ui/input_window_dialog_gtk.cc',
'browser/ui/input_window_dialog_win.cc',
+ 'browser/ui/intents/intents_model.cc',
+ 'browser/ui/intents/intents_model.h',
'browser/ui/login/login_model.h',
'browser/ui/login/login_prompt.cc',
'browser/ui/login/login_prompt.h',
@@ -3615,6 +3617,8 @@
'browser/ui/webui/options/handler_options_handler.h',
'browser/ui/webui/options/import_data_handler.cc',
'browser/ui/webui/options/import_data_handler.h',
+ 'browser/ui/webui/options/intents_settings_handler.cc',
+ 'browser/ui/webui/options/intents_settings_handler.h',
'browser/ui/webui/options/language_options_handler.cc',
'browser/ui/webui/options/language_options_handler.h',
'browser/ui/webui/options/language_options_handler_common.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index 0662b00..ddbcbec 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1868,6 +1868,7 @@
'browser/ui/gtk/reload_button_gtk_unittest.cc',
'browser/ui/gtk/status_icons/status_tray_gtk_unittest.cc',
'browser/ui/gtk/tabs/tab_renderer_gtk_unittest.cc',
+ 'browser/ui/intents/intents_model_unittest.cc',
'browser/ui/login/login_prompt_unittest.cc',
'browser/ui/omnibox/omnibox_view_unittest.cc',
'browser/ui/panels/panel_browser_window_cocoa_unittest.mm',