Commit 49d9c54f authored by mgiuca's avatar mgiuca Committed by Commit bot

App Launcher: Webstore results are now highlighted.

We now highlight (in bold) the part of the query that you typed, just as
we do with other types of queries.

Moved AppResult::UpdateFromMatch into SearchResult so it can be used by
any provider.

BUG=481837

Review URL: https://codereview.chromium.org/1110883003

Cr-Commit-Position: refs/heads/master@{#329356}
parent 2f5eaca8
...@@ -23,8 +23,6 @@ ...@@ -23,8 +23,6 @@
#include "extensions/common/extension_icon_set.h" #include "extensions/common/extension_icon_set.h"
#include "extensions/common/manifest_handlers/icons_handler.h" #include "extensions/common/manifest_handlers/icons_handler.h"
#include "ui/app_list/app_list_switches.h" #include "ui/app_list/app_list_switches.h"
#include "ui/app_list/search/tokenized_string.h"
#include "ui/app_list/search/tokenized_string_match.h"
#include "ui/gfx/color_utils.h" #include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image_skia_operations.h" #include "ui/gfx/image/image_skia_operations.h"
...@@ -65,20 +63,6 @@ AppResult::~AppResult() { ...@@ -65,20 +63,6 @@ AppResult::~AppResult() {
StopObservingExtensionRegistry(); StopObservingExtensionRegistry();
} }
void AppResult::UpdateFromMatch(const TokenizedString& title,
const TokenizedStringMatch& match) {
const TokenizedStringMatch::Hits& hits = match.hits();
Tags tags;
tags.reserve(hits.size());
for (size_t i = 0; i < hits.size(); ++i)
tags.push_back(Tag(Tag::MATCH, hits[i].start(), hits[i].end()));
set_title(title.text());
set_title_tags(tags);
set_relevance(match.relevance());
}
void AppResult::UpdateFromLastLaunched(const base::Time& current_time, void AppResult::UpdateFromLastLaunched(const base::Time& current_time,
const base::Time& last_launched) { const base::Time& last_launched) {
base::TimeDelta delta = current_time - last_launched; base::TimeDelta delta = current_time - last_launched;
...@@ -129,6 +113,7 @@ scoped_ptr<SearchResult> AppResult::Duplicate() const { ...@@ -129,6 +113,7 @@ scoped_ptr<SearchResult> AppResult::Duplicate() const {
display_type() == DISPLAY_RECOMMENDATION)); display_type() == DISPLAY_RECOMMENDATION));
copy->set_title(title()); copy->set_title(title());
copy->set_title_tags(title_tags()); copy->set_title_tags(title_tags());
copy->set_relevance(relevance());
return copy.Pass(); return copy.Pass();
} }
......
...@@ -29,8 +29,6 @@ class ExtensionRegistry; ...@@ -29,8 +29,6 @@ class ExtensionRegistry;
namespace app_list { namespace app_list {
class AppContextMenu; class AppContextMenu;
class TokenizedString;
class TokenizedStringMatch;
class AppResult : public SearchResult, class AppResult : public SearchResult,
public extensions::IconImage::Observer, public extensions::IconImage::Observer,
...@@ -44,9 +42,6 @@ class AppResult : public SearchResult, ...@@ -44,9 +42,6 @@ class AppResult : public SearchResult,
bool is_recommendation); bool is_recommendation);
~AppResult() override; ~AppResult() override;
void UpdateFromMatch(const TokenizedString& title,
const TokenizedStringMatch& match);
void UpdateFromLastLaunched(const base::Time& current_time, void UpdateFromLastLaunched(const base::Time& current_time,
const base::Time& last_launched); const base::Time& last_launched);
......
...@@ -178,17 +178,18 @@ scoped_ptr<SearchResult> WebstoreProvider::CreateResult( ...@@ -178,17 +178,18 @@ scoped_ptr<SearchResult> WebstoreProvider::CreateResult(
extensions::Manifest::Type item_type = ParseItemType(item_type_string); extensions::Manifest::Type item_type = ParseItemType(item_type_string);
// Calculate the relevance score by matching the query with the title. Results // Calculate the relevance score by matching the query with the title. Results
// with a match score of 0 are discarded. // with a match score of 0 are discarded. This will also be used to set the
// TODO(mgiuca): Set the tags to indicate the parts of the title that were // title tags (highlighting which parts of the title matched the search
// matched. // query).
TokenizedString title(base::UTF8ToUTF16(localized_name)); TokenizedString title(base::UTF8ToUTF16(localized_name));
TokenizedStringMatch match; TokenizedStringMatch match;
if (!match.Calculate(query, title)) if (!match.Calculate(query, title))
return scoped_ptr<SearchResult>(); return scoped_ptr<SearchResult>();
return make_scoped_ptr(new WebstoreResult(profile_, app_id, localized_name, scoped_ptr<SearchResult> result(new WebstoreResult(
match.relevance(), icon_url, profile_, app_id, icon_url, is_paid, item_type, controller_));
is_paid, item_type, controller_)); result->UpdateFromMatch(title, match);
return result;
} }
} // namespace app_list } // namespace app_list
...@@ -41,7 +41,7 @@ const char kOneResult[] = ...@@ -41,7 +41,7 @@ const char kOneResult[] =
"\"results\":[" "\"results\":["
" {" " {"
" \"id\": \"app1_id\"," " \"id\": \"app1_id\","
" \"localized_name\": \"app1 name\"," " \"localized_name\": \"Fun App\","
" \"icon_url\": \"http://host/icon\"," " \"icon_url\": \"http://host/icon\","
" \"is_paid\": false" " \"is_paid\": false"
" }" " }"
...@@ -83,31 +83,48 @@ struct ParsedSearchResult { ...@@ -83,31 +83,48 @@ struct ParsedSearchResult {
size_t num_actions; size_t num_actions;
}; };
// Expected results from a search for "fun" on kOneResult.
ParsedSearchResult kParsedOneResult[] = {{"app1_id", ParsedSearchResult kParsedOneResult[] = {{"app1_id",
"app1 name", "[Fun] App",
"http://host/icon", "http://host/icon",
false, false,
Manifest::TYPE_UNKNOWN, Manifest::TYPE_UNKNOWN,
1}}; 1}};
ParsedSearchResult kParsedThreeResults[] = {{"app1_id", // Expected results from a search for "app" on kThreeResults.
"Mystery App", ParsedSearchResult kParsedThreeResultsApp[] = {
"http://host/icon1", {"app1_id",
true, "Mystery [App]",
Manifest::TYPE_PLATFORM_APP, "http://host/icon1",
1}, true,
{"app2_id", Manifest::TYPE_PLATFORM_APP,
"App Mystère", 1},
"http://host/icon2", {"app2_id",
false, "[App] Mystère",
Manifest::TYPE_HOSTED_APP, "http://host/icon2",
1}, false,
{"app3_id", Manifest::TYPE_HOSTED_APP,
"Mistero App", 1},
"http://host/icon3", {"app3_id",
false, "Mistero [App]",
Manifest::TYPE_LEGACY_PACKAGED_APP, "http://host/icon3",
1}}; false,
Manifest::TYPE_LEGACY_PACKAGED_APP,
1}};
// Expected results from a search for "myst" on kThreeResults.
ParsedSearchResult kParsedThreeResultsMyst[] = {{"app1_id",
"[Myst]ery App",
"http://host/icon1",
true,
Manifest::TYPE_PLATFORM_APP,
1},
{"app2_id",
"App [Myst]ère",
"http://host/icon2",
false,
Manifest::TYPE_HOSTED_APP,
1}};
} // namespace } // namespace
...@@ -183,7 +200,8 @@ class WebstoreProviderTest : public InProcessBrowserTest { ...@@ -183,7 +200,8 @@ class WebstoreProviderTest : public InProcessBrowserTest {
expected_results[i].id).spec(), expected_results[i].id).spec(),
result->id()); result->id());
EXPECT_EQ(std::string(expected_results[i].title), EXPECT_EQ(std::string(expected_results[i].title),
base::UTF16ToUTF8(result->title())); app_list::SearchResult::TagsDebugString(
base::UTF16ToUTF8(result->title()), result->title_tags()));
// Ensure the number of action buttons is appropriate for the item type. // Ensure the number of action buttons is appropriate for the item type.
EXPECT_EQ(expected_results[i].num_actions, result->actions().size()); EXPECT_EQ(expected_results[i].num_actions, result->actions().size());
...@@ -265,18 +283,18 @@ IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, Basic) { ...@@ -265,18 +283,18 @@ IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, Basic) {
{"bad json", "invalid json", "bad json", nullptr, 0}, {"bad json", "invalid json", "bad json", nullptr, 0},
// Good results. Note that the search term appears in all of the result // Good results. Note that the search term appears in all of the result
// titles. // titles.
{"app1", kOneResult, "app1 name", kParsedOneResult, 1}, {"fun", kOneResult, "Fun App", kParsedOneResult, 1},
{"app", {"app",
kThreeResults, kThreeResults,
"Mystery App,App Mystère,Mistero App", "Mystery App,App Mystère,Mistero App",
kParsedThreeResults, kParsedThreeResultsApp,
3}, 3},
// Search where one of the results does not include the query term. Only // Search where one of the results does not include the query term. Only
// the results with a title matching the query should be selected. // the results with a title matching the query should be selected.
{"myst", {"myst",
kThreeResults, kThreeResults,
"Mystery App,App Mystère", "Mystery App,App Mystère",
kParsedThreeResults, kParsedThreeResultsMyst,
2}, 2},
}; };
...@@ -317,16 +335,16 @@ IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, NoSearchForSensitiveData) { ...@@ -317,16 +335,16 @@ IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, NoSearchForSensitiveData) {
} }
IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, NoSearchForShortQueries) { IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, NoSearchForShortQueries) {
RunQueryAndVerify("a", kOneResult, nullptr, 0); RunQueryAndVerify("f", kOneResult, nullptr, 0);
RunQueryAndVerify("ap", kOneResult, nullptr, 0); RunQueryAndVerify("fu", kOneResult, nullptr, 0);
RunQueryAndVerify("app", kOneResult, kParsedOneResult, 1); RunQueryAndVerify("fun", kOneResult, kParsedOneResult, 1);
} }
IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, SearchCache) { IN_PROC_BROWSER_TEST_F(WebstoreProviderTest, SearchCache) {
RunQueryAndVerify("app", kOneResult, kParsedOneResult, 1); RunQueryAndVerify("fun", kOneResult, kParsedOneResult, 1);
// No result is provided but the provider gets the result from the cache. // No result is provided but the provider gets the result from the cache.
RunQueryAndVerify("app", "", kParsedOneResult, 1); RunQueryAndVerify("fun", "", kParsedOneResult, 1);
} }
} // namespace test } // namespace test
......
...@@ -61,15 +61,12 @@ namespace app_list { ...@@ -61,15 +61,12 @@ namespace app_list {
WebstoreResult::WebstoreResult(Profile* profile, WebstoreResult::WebstoreResult(Profile* profile,
const std::string& app_id, const std::string& app_id,
const std::string& localized_name,
double relevance,
const GURL& icon_url, const GURL& icon_url,
bool is_paid, bool is_paid,
extensions::Manifest::Type item_type, extensions::Manifest::Type item_type,
AppListControllerDelegate* controller) AppListControllerDelegate* controller)
: profile_(profile), : profile_(profile),
app_id_(app_id), app_id_(app_id),
localized_name_(localized_name),
icon_url_(icon_url), icon_url_(icon_url),
is_paid_(is_paid), is_paid_(is_paid),
item_type_(item_type), item_type_(item_type),
...@@ -78,9 +75,7 @@ WebstoreResult::WebstoreResult(Profile* profile, ...@@ -78,9 +75,7 @@ WebstoreResult::WebstoreResult(Profile* profile,
extension_registry_(NULL), extension_registry_(NULL),
weak_factory_(this) { weak_factory_(this) {
set_id(extensions::Extension::GetBaseURLFromExtensionId(app_id_).spec()); set_id(extensions::Extension::GetBaseURLFromExtensionId(app_id_).spec());
set_relevance(relevance);
set_title(base::UTF8ToUTF16(localized_name_));
SetDefaultDetails(); SetDefaultDetails();
InitAndStartObserving(); InitAndStartObserving();
...@@ -128,9 +123,12 @@ void WebstoreResult::InvokeAction(int action_index, int event_flags) { ...@@ -128,9 +123,12 @@ void WebstoreResult::InvokeAction(int action_index, int event_flags) {
} }
scoped_ptr<SearchResult> WebstoreResult::Duplicate() const { scoped_ptr<SearchResult> WebstoreResult::Duplicate() const {
return scoped_ptr<SearchResult>( scoped_ptr<SearchResult> copy(new WebstoreResult(
new WebstoreResult(profile_, app_id_, localized_name_, relevance(), profile_, app_id_, icon_url_, is_paid_, item_type_, controller_));
icon_url_, is_paid_, item_type_, controller_)); copy->set_title(title());
copy->set_title_tags(title_tags());
copy->set_relevance(relevance());
return copy;
} }
void WebstoreResult::InitAndStartObserving() { void WebstoreResult::InitAndStartObserving() {
......
...@@ -32,8 +32,6 @@ class WebstoreResult : public SearchResult, ...@@ -32,8 +32,6 @@ class WebstoreResult : public SearchResult,
public: public:
WebstoreResult(Profile* profile, WebstoreResult(Profile* profile,
const std::string& app_id, const std::string& app_id,
const std::string& localized_name,
double relevance,
const GURL& icon_url, const GURL& icon_url,
bool is_paid, bool is_paid,
extensions::Manifest::Type item_type, extensions::Manifest::Type item_type,
...@@ -82,7 +80,6 @@ class WebstoreResult : public SearchResult, ...@@ -82,7 +80,6 @@ class WebstoreResult : public SearchResult,
Profile* profile_; Profile* profile_;
const std::string app_id_; const std::string app_id_;
const std::string localized_name_;
const GURL icon_url_; const GURL icon_url_;
const bool is_paid_; const bool is_paid_;
extensions::Manifest::Type item_type_; extensions::Manifest::Type item_type_;
......
...@@ -4,7 +4,11 @@ ...@@ -4,7 +4,11 @@
#include "ui/app_list/search_result.h" #include "ui/app_list/search_result.h"
#include <map>
#include "ui/app_list/app_list_constants.h" #include "ui/app_list/app_list_constants.h"
#include "ui/app_list/search/tokenized_string.h"
#include "ui/app_list/search/tokenized_string_match.h"
#include "ui/app_list/search_result_observer.h" #include "ui/app_list/search_result_observer.h"
namespace app_list { namespace app_list {
...@@ -99,6 +103,20 @@ void SearchResult::RemoveObserver(SearchResultObserver* observer) { ...@@ -99,6 +103,20 @@ void SearchResult::RemoveObserver(SearchResultObserver* observer) {
observers_.RemoveObserver(observer); observers_.RemoveObserver(observer);
} }
void SearchResult::UpdateFromMatch(const TokenizedString& title,
const TokenizedStringMatch& match) {
const TokenizedStringMatch::Hits& hits = match.hits();
Tags tags;
tags.reserve(hits.size());
for (size_t i = 0; i < hits.size(); ++i)
tags.push_back(Tag(Tag::MATCH, hits[i].start(), hits[i].end()));
set_title(title.text());
set_title_tags(tags);
set_relevance(match.relevance());
}
void SearchResult::Open(int event_flags) { void SearchResult::Open(int event_flags) {
} }
...@@ -109,4 +127,33 @@ ui::MenuModel* SearchResult::GetContextMenuModel() { ...@@ -109,4 +127,33 @@ ui::MenuModel* SearchResult::GetContextMenuModel() {
return NULL; return NULL;
} }
// static
std::string SearchResult::TagsDebugString(const std::string& text,
const Tags& tags) {
std::string result = text;
// Build a table of delimiters to insert.
std::map<size_t, std::string> inserts;
for (const auto& tag : tags) {
if (tag.styles & Tag::URL)
inserts[tag.range.start()].push_back('{');
if (tag.styles & Tag::MATCH)
inserts[tag.range.start()].push_back('[');
if (tag.styles & Tag::DIM) {
inserts[tag.range.start()].push_back('<');
inserts[tag.range.end()].push_back('>');
}
if (tag.styles & Tag::MATCH)
inserts[tag.range.end()].push_back(']');
if (tag.styles & Tag::URL)
inserts[tag.range.end()].push_back('}');
}
// Insert the delimiters (in reverse order, to preserve indices).
for (auto it = inserts.rbegin(); it != inserts.rend(); ++it)
result.insert(it->first, it->second);
return result;
}
} // namespace app_list } // namespace app_list
...@@ -22,6 +22,8 @@ class MenuModel; ...@@ -22,6 +22,8 @@ class MenuModel;
namespace app_list { namespace app_list {
class SearchResultObserver; class SearchResultObserver;
class TokenizedString;
class TokenizedStringMatch;
// SearchResult consists of an icon, title text and details text. Title and // SearchResult consists of an icon, title text and details text. Title and
// details text can have tagged ranges that are displayed differently from // details text can have tagged ranges that are displayed differently from
...@@ -135,6 +137,11 @@ class APP_LIST_EXPORT SearchResult { ...@@ -135,6 +137,11 @@ class APP_LIST_EXPORT SearchResult {
void AddObserver(SearchResultObserver* observer); void AddObserver(SearchResultObserver* observer);
void RemoveObserver(SearchResultObserver* observer); void RemoveObserver(SearchResultObserver* observer);
// Updates the result's relevance score, and sets its title and title tags,
// based on a string match result.
void UpdateFromMatch(const TokenizedString& title,
const TokenizedStringMatch& match);
// TODO(mukai): Remove this method and really simplify the ownership of // TODO(mukai): Remove this method and really simplify the ownership of
// SearchResult. Ideally, SearchResult will be copyable. // SearchResult. Ideally, SearchResult will be copyable.
virtual scoped_ptr<SearchResult> Duplicate() const = 0; virtual scoped_ptr<SearchResult> Duplicate() const = 0;
...@@ -147,6 +154,10 @@ class APP_LIST_EXPORT SearchResult { ...@@ -147,6 +154,10 @@ class APP_LIST_EXPORT SearchResult {
// Note the returned menu model is owned by this item. // Note the returned menu model is owned by this item.
virtual ui::MenuModel* GetContextMenuModel(); virtual ui::MenuModel* GetContextMenuModel();
// Returns a string showing |text| marked up with brackets indicating the
// tag positions in |tags|. Useful for debugging and testing.
static std::string TagsDebugString(const std::string& text, const Tags& tags);
protected: protected:
void set_id(const std::string& id) { id_ = id; } void set_id(const std::string& id) { id_ = id; }
void set_voice_result(bool voice_result) { voice_result_ = voice_result; } void set_voice_result(bool voice_result) { voice_result_ = voice_result; }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment