Open guidomodarelli opened 6 months ago
This document describes how to dynamically add custom commands to the application menu in Chromium. It covers the files and classes that we would need to modify, the placement of menus and commands, and how we would allow adding new entries to the menu at runtime.
The main files and classes we need to modify are:
//chrome/browser/ui/toolbar/app_menu_model.cc
: This class defines the application menu entries. We need to modify the Build()
function to add our own entries to the menu.//chrome/app/chromium_strings.grd
: This file defines the text strings displayed in the user interface. We need to add our own strings for the new menu entries.//chrome/browser/ui/views/toolbar/app_menu.cc
: This class defines how the application menu is displayed and how user interactions with it are handled. We need to modify the ExecuteCommand()
function to define what happens when the user selects our new menu entries.The reason you need to modify ExecuteCommand()
in chrome/browser/ui/views/toolbar/app_menu.cc
instead of chrome/browser/ui/toolbar/app_menu_model.cc
is due to how the code is organized in Chromium.
In Chromium, AppMenuModel
(defined in app_menu_model.cc
) is responsible for building the application menu, that is, defining which items appear in the menu and in what order. However, AppMenuModel
is not responsible for handling the actions that occur when a menu item is selected. That responsibility falls on the AppMenu
class (defined in app_menu.cc
).
Therefore, if you want to change what happens when a menu item is selected, you need to modify AppMenu::ExecuteCommand()
. This function is called when a menu item is selected, and its job is to determine what action should be taken based on the command identifier of the menu item.
On the other hand, if you only want to change the items that appear in the menu, but not what happens when they are selected, then you would only need to modify AppMenuModel::Build()
.
To dynamically add new entries to the application menu, we need to modify the Build()
function in AppMenuModel
to query a list of custom commands that can be updated at runtime. Here's an example of how we could do this:
void AppMenuModel::Build() {
// ... existing code to build the menu ...
// Add custom entries to the menu.
for (const auto& command : custom_commands_) {
AddItem(command.id, l10n_util::GetStringUTF16(command.string_id));
}
// ... more code to build the rest of the menu ...
}
In this example, custom_commands_
is a list of structures representing the custom commands. Each structure has an id
that is a unique identifier for the command, and a string_id
that is an identifier for the text string to be displayed in the menu.
custom_commands_
?Below is an example of a possible implementation:
struct CustomCommand {
int id; // The unique identifier of the command.
int string_id; // The identifier of the text string to be displayed in the menu.
base::RepeatingCallback<void()> callback; // The function to be called when the command is selected.
};
std::vector<CustomCommand> custom_commands_;
In this example, CustomCommand
is a structure representing a custom command. Each command has a unique identifier, a string identifier, and a callback function that will be invoked when the command is selected.
custom_commands_
is a list of these custom commands. It could be defined as a member of the AppMenuModel
class, so that it's available in the Build()
and ExecuteCommand()
functions.
To add a new command to the list, you could do something like this:
custom_commands_.push_back({IDC_MY_CUSTOM_COMMAND, IDS_MY_CUSTOM_COMMAND, base::BindRepeating(&MyApplication::HandleMyCustomCommand, base::Unretained(my_application))});
In this example, IDC_MY_CUSTOM_COMMAND
and IDS_MY_CUSTOM_COMMAND
are the command and string identifiers respectively, and MyApplication::HandleMyCustomCommand
is the function that will be called when the command is selected. my_application
is an instance of the MyApplication
class that contains the HandleMyCustomCommand
function.
The definition of the CustomCommand
structure and the declaration of the custom_commands_
class member should go in the app_menu_model.h
header file.
An example of how we could do this:
// In app_menu_model.h
#include "base/callback.h"
#include <vector>
// ... other includes ...
class AppMenuModel {
public:
// ... other class declarations ...
struct CustomCommand {
int id; // The unique identifier of the command.
int string_id; // The identifier of the text string to be displayed in the menu.
base::RepeatingCallback<void()> callback; // The function to be called when the command is selected.
};
// ... more class declarations ...
private:
// ... other class declarations ...
std::vector<CustomCommand> custom_commands_;
// ... more class declarations ...
};
To define text strings for our new menu entries, we need to modify the chromium_strings.grd
file to add new strings as needed. However, since we are adding commands dynamically, we also need a way to add text strings at runtime.
One way to do this would be to have a function in our application that can generate unique string identifiers and register new strings with those identifiers. Then, we could use these identifiers when adding our entries to the menu.
class MyApplication {
public:
// Registers a new string and returns its identifier.
int RegisterString(const std::string& str) {
int id = NextStringId();
// Registers the string with the new identifier.
string_resources_[id] = str;
return id;
}
// Retrieves a registered string.
std::string GetString(int id) {
auto it = string_resources_.find(id);
if (it != string_resources_.end()) {
return it->second;
} else {
// Handles the case where `id` is not found in `string_resources_`.
// You could throw an exception, return an empty string, etc.
return std::string();
}
}
private:
// Returns the next available unique string identifier.
int NextStringId() {
// We need to ensure this identifier doesn't conflict with any existing string identifiers.
return next_string_id_++;
}
// `next_string_id_` keeps track of the next available string identifier.
int next_string_id_ = IDS_CONTENT_CONTEXT_CUSTOM_FIRST;
std::map<int, std::string> string_resources_;
};
In the pseudocode, string_resources_
is a map from string identifiers to strings. When we register a new string with RegisterString()
, we add it to this map. Then, when we need to retrieve the string, we look it up in the map with GetString()
.
To define what happens when the user selects our new menu entries, we need to modify the ExecuteCommand()
function in AppMenu
. Here's an example of how we could do this:
void AppMenu::ExecuteCommand(int command_id, int event_flags) {
// ... existing code to handle existing commands ...
// Handle custom commands.
for (const auto& command : custom_commands_) {
if (command_id == command.id) {
// Here goes the code to handle the custom command.
return;
}
}
// ... more code to handle the rest of the commands ...
}
In this example, custom_commands_
is the same list of custom commands we use in Build()
.
To allow the addition of new entries to the menu at runtime, we could add a new function to AppMenuModel
that allows adding commands to the custom_commands_
list. Here's an example of how we could do this:
void AppMenuModel::AddCustomCommand(int command_id, int string_id, base::RepeatingCallback<void()> callback) {
custom_commands_.push_back({command_id, string_id, std::move(callback)});
// Rebuilds the menu to reflect the changes.
Build();
}
In this example, callback
is an additional parameter passed to AddCustomCommand
. Then, when we add the new command to custom_commands_
, we move the callback to the new command with std::move(callback)
. This is to avoid unnecessary copies of the callback, as callbacks can be heavyweight objects.
Then, to add a new command, you could do something like this:
app_menu_model.AddCustomCommand(IDC_MY_CUSTOM_COMMAND, IDS_MY_CUSTOM_COMMAND, base::BindRepeating(&MyApplication::HandleMyCustomCommand, base::Unretained(my_application)));
In this example, MyApplication::HandleMyCustomCommand
is the function that will be called when the command is selected.
Decide whether to delegate to the application or provide an API
The contextual menu modified by @agustinschilling is similar; it has identifiers for static commands, but it also has configurable dynamic command identifiers. I recall that the documentation says something like there is a range of identifiers that the user can use, and below that range are the standard Chrome ones. The identifiers shown here
are the ones we should use.