| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/search_engines/template_url_table_model.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/rtl.h" |
| #include "base/strings/string_util.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/omnibox/browser/omnibox_field_trial.h" |
| #include "components/omnibox/common/omnibox_feature_configs.h" |
| #include "components/search_engines/template_url.h" |
| #include "components/search_engines/template_url_data.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "components/search_engines/template_url_starter_pack_data.h" |
| #include "third_party/icu/source/common/unicode/locid.h" |
| #include "third_party/icu/source/i18n/unicode/coll.h" |
| #include "third_party/icu/source/i18n/unicode/ucol.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/table_model_observer.h" |
| |
| namespace internal { |
| |
| OrderByManagedAndAlphabetically::OrderByManagedAndAlphabetically() { |
| UErrorCode error_code = U_ZERO_ERROR; |
| collator_.reset( |
| icu::Collator::createInstance(icu::Locale::getDefault(), error_code)); |
| if (!U_SUCCESS(error_code)) { |
| collator_.reset(); |
| } |
| if (collator_) { |
| // Case-insensitive, ignoring diacriticals. |
| collator_->setStrength(icu::Collator::PRIMARY); |
| } |
| } |
| |
| OrderByManagedAndAlphabetically::OrderByManagedAndAlphabetically( |
| const OrderByManagedAndAlphabetically& other) |
| : collator_(other.collator_->clone()) {} |
| |
| OrderByManagedAndAlphabetically::~OrderByManagedAndAlphabetically() = default; |
| |
| bool OrderByManagedAndAlphabetically::operator()(const TemplateURL* lhs, |
| const TemplateURL* rhs) const { |
| auto get_sort_key = [this](const TemplateURL* engine) { |
| return std::make_tuple( |
| // Enterprise search engines are shown before other engines. |
| !engine->CreatedByNonDefaultSearchProviderPolicy(), |
| // Try to compare short names ignoring case and diacriticals. |
| collator_ ? GetShortNameSortKey(engine->short_name()) : std::string(), |
| // If a collator is not available, fallback to regular string |
| // comparison. |
| engine->short_name(), |
| // If short name is the same, fallback to keyword. |
| engine->keyword()); |
| }; |
| return get_sort_key(lhs) < get_sort_key(rhs); |
| } |
| |
| std::string OrderByManagedAndAlphabetically::GetShortNameSortKey( |
| const std::u16string& short_name) const { |
| CHECK(collator_); |
| |
| constexpr int32_t kBufferSize = 1000; |
| uint8_t buffer[kBufferSize]; |
| icu::UnicodeString icu_str(short_name.c_str(), short_name.length()); |
| |
| int32_t sort_key_length = collator_->getSortKey(icu_str, buffer, kBufferSize); |
| |
| // If the sort key is too long for our buffer, trim the original string |
| // for comparison to avoid buffer overflow. |
| if (sort_key_length >= kBufferSize) { |
| buffer[kBufferSize - 1] = 0; |
| sort_key_length = kBufferSize; |
| } |
| |
| // getSortKey returns the length including null terminator, but we want |
| // to exclude it from the string to avoid issues with string comparison. |
| if (sort_key_length > 0) { |
| sort_key_length--; |
| } |
| |
| return std::string(reinterpret_cast<const char*>(buffer), sort_key_length); |
| } |
| |
| } // namespace internal |
| |
| TemplateURLTableModel::TemplateURLTableModel( |
| TemplateURLService* template_url_service, |
| bool ai_mode_enabled) |
| : observer_(nullptr), |
| template_url_service_(template_url_service), |
| ai_mode_enabled_(ai_mode_enabled) { |
| DCHECK(template_url_service); |
| template_url_service_->AddObserver(this); |
| template_url_service_->Load(); |
| Reload(); |
| } |
| |
| TemplateURLTableModel::~TemplateURLTableModel() { |
| template_url_service_->RemoveObserver(this); |
| } |
| |
| void TemplateURLTableModel::Reload() { |
| TemplateURL::TemplateURLVector urls = |
| template_url_service_->GetTemplateURLs(); |
| |
| TemplateURL::TemplateURLVector default_entries, active_entries, other_entries, |
| extension_entries; |
| // Keywords that can be made the default first. |
| for (TemplateURL* template_url : urls) { |
| // Skip @gemini if feature disabled. |
| if (template_url->starter_pack_id() == |
| template_url_starter_pack_data::kGemini && |
| !OmniboxFieldTrial::IsStarterPackExpansionEnabled()) { |
| continue; |
| } |
| // Skip @page if feature disabled. |
| if (template_url->starter_pack_id() == |
| template_url_starter_pack_data::kPage && |
| !omnibox_feature_configs::ContextualSearch::Get().starter_pack_page) { |
| continue; |
| } |
| // Skip @aimode if feature disabled. |
| if (template_url->starter_pack_id() == |
| template_url_starter_pack_data::kAiMode && |
| !ai_mode_enabled_) { |
| continue; |
| } |
| |
| if (template_url_service_->ShowInDefaultList(template_url)) { |
| default_entries.push_back(template_url); |
| } else if (!template_url_service_->HiddenFromLists(template_url)) { |
| if (template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION) { |
| extension_entries.push_back(template_url); |
| } else if (template_url_service_->ShowInActivesList(template_url)) { |
| active_entries.push_back(template_url); |
| } else { |
| other_entries.push_back(template_url); |
| } |
| } |
| } |
| |
| std::ranges::sort(active_entries, |
| internal::OrderByManagedAndAlphabetically()); |
| std::ranges::sort(other_entries, internal::OrderByManagedAndAlphabetically()); |
| |
| last_search_engine_index_ = default_entries.size(); |
| last_active_engine_index_ = last_search_engine_index_ + active_entries.size(); |
| last_other_engine_index_ = last_active_engine_index_ + other_entries.size(); |
| |
| entries_.clear(); |
| std::move(default_entries.begin(), default_entries.end(), |
| std::back_inserter(entries_)); |
| |
| std::move(active_entries.begin(), active_entries.end(), |
| std::back_inserter(entries_)); |
| |
| std::move(other_entries.begin(), other_entries.end(), |
| std::back_inserter(entries_)); |
| |
| std::move(extension_entries.begin(), extension_entries.end(), |
| std::back_inserter(entries_)); |
| |
| if (observer_) { |
| observer_->OnModelChanged(); |
| } |
| } |
| |
| size_t TemplateURLTableModel::RowCount() { |
| return entries_.size(); |
| } |
| |
| std::u16string TemplateURLTableModel::GetText(size_t row, int col_id) { |
| DCHECK(row < RowCount()); |
| const TemplateURL* url = entries_[row]; |
| if (col_id == IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN) { |
| std::u16string url_short_name = url->short_name(); |
| // TODO(xji): Consider adding a special case if the short name is a URL, |
| // since those should always be displayed LTR. Please refer to |
| // http://crbug.com/6726 for more information. |
| base::i18n::AdjustStringForLocaleDirection(&url_short_name); |
| return (template_url_service_->GetDefaultSearchProvider() == url) |
| ? l10n_util::GetStringFUTF16( |
| IDS_SEARCH_ENGINES_EDITOR_DEFAULT_ENGINE, url_short_name) |
| : url_short_name; |
| } |
| |
| DCHECK_EQ(IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN, col_id); |
| // Keyword should be domain name. Force it to have LTR directionality. |
| return base::i18n::GetDisplayStringInLTRDirectionality(url->keyword()); |
| } |
| |
| void TemplateURLTableModel::SetObserver(ui::TableModelObserver* observer) { |
| observer_ = observer; |
| } |
| |
| void TemplateURLTableModel::Remove(size_t index) { |
| TemplateURL* template_url = GetTemplateURL(index); |
| template_url_service_->Remove(template_url); |
| } |
| |
| void TemplateURLTableModel::Add(size_t index, |
| const std::u16string& short_name, |
| const std::u16string& keyword, |
| const std::string& url) { |
| DCHECK(index <= RowCount()); |
| DCHECK(!url.empty()); |
| TemplateURLData data; |
| data.SetShortName(short_name); |
| data.SetKeyword(keyword); |
| data.SetURL(url); |
| data.is_active = TemplateURLData::ActiveStatus::kTrue; |
| template_url_service_->Add(std::make_unique<TemplateURL>(data)); |
| } |
| |
| void TemplateURLTableModel::ModifyTemplateURL(size_t index, |
| const std::u16string& title, |
| const std::u16string& keyword, |
| const std::string& url) { |
| DCHECK(index <= RowCount()); |
| DCHECK(!url.empty()); |
| TemplateURL* template_url = GetTemplateURL(index); |
| |
| // The default search provider should support replacement. |
| DCHECK(template_url_service_->GetDefaultSearchProvider() != template_url || |
| template_url->SupportsReplacement( |
| template_url_service_->search_terms_data())); |
| template_url_service_->ResetTemplateURL(template_url, title, keyword, url); |
| } |
| |
| TemplateURL* TemplateURLTableModel::GetTemplateURL(size_t index) { |
| // Sanity checks for https://crbug.com/781703. |
| CHECK_LT(index, entries_.size()); |
| CHECK( |
| base::Contains(template_url_service_->GetTemplateURLs(), entries_[index])) |
| << "TemplateURLTableModel is returning a pointer to a TemplateURL " |
| "that has already been freed by TemplateURLService."; |
| |
| return entries_[index]; |
| } |
| |
| std::optional<size_t> TemplateURLTableModel::IndexOfTemplateURL( |
| const TemplateURL* template_url) { |
| for (auto i = entries_.begin(); i != entries_.end(); ++i) { |
| if (*i == template_url) { |
| return static_cast<size_t>(i - entries_.begin()); |
| } |
| } |
| return std::nullopt; |
| } |
| |
| void TemplateURLTableModel::MakeDefaultTemplateURL( |
| size_t index, |
| search_engines::ChoiceMadeLocation choice_location) { |
| DCHECK_LT(index, RowCount()); |
| |
| TemplateURL* keyword = GetTemplateURL(index); |
| const TemplateURL* current_default = |
| template_url_service_->GetDefaultSearchProvider(); |
| if (current_default == keyword) { |
| return; |
| } |
| |
| template_url_service_->SetUserSelectedDefaultSearchProvider(keyword, |
| choice_location); |
| } |
| |
| void TemplateURLTableModel::SetIsActiveTemplateURL(size_t index, |
| bool is_active) { |
| DCHECK(index <= RowCount()); |
| TemplateURL* keyword = GetTemplateURL(index); |
| |
| template_url_service_->SetIsActiveTemplateURL(keyword, is_active); |
| } |
| |
| void TemplateURLTableModel::OnTemplateURLServiceChanged() { |
| Reload(); |
| } |