Skip to content

Commit

Permalink
Omnibox Command Palette (#16947)
Browse files Browse the repository at this point in the history
* Add Commander to Omnibox
  • Loading branch information
fallaciousreasoning authored Mar 31, 2023
1 parent a40703b commit 3c22732
Show file tree
Hide file tree
Showing 50 changed files with 1,827 additions and 19 deletions.
1 change: 1 addition & 0 deletions browser/about_flags.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "brave/components/skus/common/features.h"
#include "brave/components/speedreader/common/buildflags/buildflags.h"
#include "build/build_config.h"
#include "chrome/browser/ui/ui_features.h"
#include "components/content_settings/core/common/features.h"
#include "components/flags_ui/feature_entry.h"
#include "components/flags_ui/feature_entry_macros.h"
Expand Down
1 change: 1 addition & 0 deletions browser/sources.gni
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ brave_chrome_browser_deps = [

if (!is_android && !is_ios) {
brave_chrome_browser_deps += [
"//brave/components/commander/browser",
"//brave/components/commands/browser",
"//brave/components/commands/common",
]
Expand Down
8 changes: 8 additions & 0 deletions browser/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ source_set("ui") {
"browser_commands.cc",
"browser_commands.h",
"browser_dialogs.h",
"commander/commander_service.cc",
"commander/commander_service.h",
"commander/commander_service_factory.cc",
"commander/commander_service_factory.h",
"content_settings/brave_autoplay_blocked_image_model.cc",
"content_settings/brave_autoplay_blocked_image_model.h",
"content_settings/brave_autoplay_content_setting_bubble_model.cc",
Expand Down Expand Up @@ -535,6 +539,10 @@ source_set("ui") {
deps += [ "//brave/components/text_recognition/browser" ]
}

if (!is_android && !is_ios) {
deps += [ "//brave/components/commander/browser" ]
}

# This is no longer compiled into Chromium on Android, but we still
# need it
if (is_android) {
Expand Down
24 changes: 24 additions & 0 deletions browser/ui/commander/BUILD.gn
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) 2023 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.

source_set("browser_tests") {
testonly = true
defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]

sources = [ "commander_service_browsertest.cc" ]

deps = [
"//base",
"//brave/components/commander/browser",
"//chrome/browser/profiles",
"//chrome/browser/profiles:profile",
"//chrome/browser/ui",
"//chrome/test:test_support_ui",
"//components/omnibox/browser",
"//content/test:test_support",
"//testing/gmock",
"//testing/gtest",
]
}
255 changes: 255 additions & 0 deletions browser/ui/commander/commander_service.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Copyright (c) 2023 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

#include "brave/browser/ui/commander/commander_service.h"

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/weak_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "brave/components/commander/browser/commander_frontend_delegate.h"
#include "brave/components/commander/browser/commander_item_model.h"
#include "brave/components/commander/common/constants.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/commander/bookmark_command_source.h"
#include "chrome/browser/ui/commander/command_source.h"
#include "chrome/browser/ui/commander/commander.h"
#include "chrome/browser/ui/commander/commander_view_model.h"
#include "chrome/browser/ui/commander/open_url_command_source.h"
#include "chrome/browser/ui/commander/simple_command_source.h"
#include "chrome/browser/ui/commander/tab_command_source.h"
#include "chrome/browser/ui/commander/window_command_source.h"
#include "chrome/browser/ui/location_bar/location_bar.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_view.h"

namespace commander {
namespace {
constexpr size_t kMaxResults = 8;
CommandItemModel FromCommand(const std::unique_ptr<CommandItem>& item) {
return CommandItemModel(item->title, item->matched_ranges, item->annotation);
}
} // namespace

CommanderService::CommanderService(Profile* profile) : profile_(profile) {
command_sources_.push_back(std::make_unique<SimpleCommandSource>());
command_sources_.push_back(std::make_unique<OpenURLCommandSource>());
command_sources_.push_back(std::make_unique<BookmarkCommandSource>());
command_sources_.push_back(std::make_unique<WindowCommandSource>());
command_sources_.push_back(std::make_unique<TabCommandSource>());
}

CommanderService::~CommanderService() = default;

void CommanderService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
observer->OnCommanderUpdated();
}

void CommanderService::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}

void CommanderService::UpdateText(bool force) {
auto* browser = chrome::FindLastActiveWithProfile(profile_);

// The last active browser can have no tabs, if we're in the process of moving
// the last tab from the current window into another one.
if (!browser || browser->tab_strip_model()->empty()) {
return;
}

auto* window = browser->window();
CHECK(window);

auto text = window->GetLocationBar()->GetOmniboxView()->GetText();
if (!base::StartsWith(text, kCommandPrefix)) {
return;
}

std::u16string trimmed_text(base::TrimWhitespace(
text.substr(kCommandPrefix.size()), base::TRIM_LEADING));

// If nothing has changed (and we aren't forcing things), don't update the
// commands.
if (trimmed_text == last_searched_ && browser == last_browser_ && !force) {
return;
}
last_searched_ = trimmed_text;
last_browser_ = browser;

UpdateCommands();
}

void CommanderService::SelectCommand(uint32_t command_index,
uint32_t result_set_id) {
if (command_index >= items_.size() ||
result_set_id != current_result_set_id_) {
return;
}

// Increment the current result set id - we don't want any commands from this
// set to be reused after we've selected a command.
// Note: This needs to happen before beginning a composite command, to ensure
// that the generated model uses right right |result_set_id|.
current_result_set_id_++;

auto* item = items_[command_index].get();
if (item->GetType() == CommandItem::Type::kOneShot) {
std::move(absl::get<base::OnceClosure>(item->command)).Run();
Hide();
} else {
auto composite_command =
absl::get<CommandItem::CompositeCommand>(item->command);
std::tie(prompt_, composite_command_provider_) = composite_command;
Show();
}
}

std::vector<CommandItemModel> CommanderService::GetItems() {
std::vector<CommandItemModel> result;
base::ranges::transform(items_, std::back_inserter(result), FromCommand);
return result;
}

int CommanderService::GetResultSetId() {
return current_result_set_id_;
}

const std::u16string& CommanderService::GetPrompt() {
return prompt_;
}

void CommanderService::Shutdown() {
weak_ptr_factory_.InvalidateWeakPtrs();
}

void CommanderService::Toggle() {
if (IsShowing()) {
Hide();
} else {
Show();
}
}

void CommanderService::Show() {
// Note: This posts a task because we can't change the Omnibox text while
// autocompleting.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&CommanderService::ShowCommander,
weak_ptr_factory_.GetWeakPtr()));
}

void CommanderService::Hide() {
// Note: This posts a task because we can't change the Omnibox text while
// autocompleting.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&CommanderService::HideCommander,
weak_ptr_factory_.GetWeakPtr()));
}

void CommanderService::Reset() {
current_result_set_id_++;
items_.clear();
prompt_.clear();
last_searched_.clear();
last_browser_ = nullptr;
if (composite_command_provider_) {
composite_command_provider_.Reset();
}
NotifyObservers();
}

OmniboxView* CommanderService::GetOmnibox() const {
auto* browser = chrome::FindLastActiveWithProfile(profile_);
if (!browser) {
return nullptr;
}

auto* window = browser->window();
CHECK(window);
return window->GetLocationBar()->GetOmniboxView();
}

bool CommanderService::IsShowing() const {
auto* omnibox = GetOmnibox();
return omnibox && omnibox->GetText().starts_with(kCommandPrefix.data());
}

void CommanderService::UpdateCommands() {
std::vector<std::unique_ptr<CommandItem>> items;
if (composite_command_provider_) {
items = composite_command_provider_.Run(last_searched_);
} else {
for (auto& source : command_sources_) {
auto commands = source->GetCommands(last_searched_, last_browser_);
items.insert(items.end(), std::make_move_iterator(commands.begin()),
std::make_move_iterator(commands.end()));
}
}

// Sort at most |kMaxResults| by score and then alphabetically.
auto max_elements = std::min(items.size(), kMaxResults);
std::partial_sort(
std::begin(items), std::begin(items) + max_elements, std::end(items),
[](const std::unique_ptr<CommandItem>& left,
const std::unique_ptr<CommandItem>& right) {
return (left->score == right->score) ? left->title < right->title
: left->score > right->score;
});
if (items.size() > kMaxResults) {
items.resize(kMaxResults);
}
items_ = std::move(items);

// Increment the current result set id, so we don't confuse these results with
// a prior set before notifying observers.
current_result_set_id_++;
NotifyObservers();
}

void CommanderService::NotifyObservers() {
for (auto& observer : observers_) {
observer.OnCommanderUpdated();
}
}

void CommanderService::ShowCommander() {
if (auto* omnibox = GetOmnibox()) {
omnibox->SetFocus(true);

auto text = base::StrCat({commander::kCommandPrefix, u" "});
omnibox->SetUserText(text);
omnibox->SetCaretPos(text.size());
UpdateText(true);
}
}

void CommanderService::HideCommander() {
Reset();

if (auto* omnibox = GetOmnibox(); omnibox && IsShowing()) {
omnibox->RevertAll();
}
}

} // namespace commander
80 changes: 80 additions & 0 deletions browser/ui/commander/commander_service.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) 2023 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

#ifndef BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_H_
#define BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_H_

#include <memory>
#include <string>
#include <vector>

#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "brave/components/commander/browser/commander_frontend_delegate.h"
#include "brave/components/commander/browser/commander_item_model.h"
#include "chrome/browser/ui/commander/command_source.h"
#include "components/keyed_service/core/keyed_service.h"

class OmniboxView;
class Profile;

namespace commander {

class CommanderService : public CommanderFrontendDelegate, public KeyedService {
public:
using CommandSources = std::vector<std::unique_ptr<CommandSource>>;

explicit CommanderService(Profile* profile);
CommanderService(const CommanderService&) = delete;
CommanderService& operator=(const CommanderService&) = delete;
~CommanderService() override;

void Show();
void Reset();
bool IsShowing() const;

// CommanderFrontendDelegate:
void Toggle() override;
void Hide() override;
void AddObserver(Observer* observer) override;
void RemoveObserver(Observer* observer) override;
void UpdateText(bool force = false) override;
void SelectCommand(uint32_t command_index, uint32_t result_set_id) override;
std::vector<CommandItemModel> GetItems() override;
int GetResultSetId() override;
const std::u16string& GetPrompt() override;

// KeyedService:
void Shutdown() override;

private:
OmniboxView* GetOmnibox() const;
void UpdateCommands();
void NotifyObservers();

void ShowCommander();
void HideCommander();

CommandSources command_sources_;

std::u16string last_searched_;
std::u16string prompt_;
std::vector<std::unique_ptr<CommandItem>> items_;
uint32_t current_result_set_id_ = 0;
raw_ptr<Browser> last_browser_;
raw_ptr<Profile> profile_;

// Some commands have multiple steps (like move tab to window, pick a
// window). This allows commands to specify a command provider for a
// subsequent step (in the window example, this would be a list of all
// available windows).
CommandItem::CompositeCommandProvider composite_command_provider_;

base::ObserverList<Observer> observers_;
base::WeakPtrFactory<CommanderService> weak_ptr_factory_{this};
};
} // namespace commander

#endif // BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_H_
Loading

0 comments on commit 3c22732

Please sign in to comment.