blob: c453da443dadbdd1820ddb4afaafae5f6fa70520 [file] [log] [blame]
// Copyright (c) 2012 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/toolbar/toolbar_actions_model.h"
#include <algorithm>
#include <string>
#include "base/location.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/extension_action_manager.h"
#include "chrome/browser/extensions/extension_message_bubble_controller.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/extensions/extension_action_view_controller.h"
#include "chrome/browser/ui/extensions/extension_message_bubble_factory.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/component_toolbar_actions_factory.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_model_factory.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/pref_names.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/one_shot_event.h"
ToolbarActionsModel::ToolbarActionsModel(
Profile* profile,
extensions::ExtensionPrefs* extension_prefs)
: profile_(profile),
extension_prefs_(extension_prefs),
prefs_(profile_->GetPrefs()),
extension_action_api_(extensions::ExtensionActionAPI::Get(profile_)),
extension_registry_(extensions::ExtensionRegistry::Get(profile_)),
extension_action_manager_(
extensions::ExtensionActionManager::Get(profile_)),
component_actions_factory_(
base::MakeUnique<ComponentToolbarActionsFactory>(profile_)),
actions_initialized_(false),
highlight_type_(HIGHLIGHT_NONE),
has_active_bubble_(false),
extension_action_observer_(this),
extension_registry_observer_(this),
weak_ptr_factory_(this) {
extensions::ExtensionSystem::Get(profile_)->ready().Post(
FROM_HERE, base::Bind(&ToolbarActionsModel::OnReady,
weak_ptr_factory_.GetWeakPtr()));
visible_icon_count_ =
prefs_->GetInteger(extensions::pref_names::kToolbarSize);
// We only care about watching the prefs if not in incognito mode.
if (!profile_->IsOffTheRecord()) {
pref_change_registrar_.Init(prefs_);
pref_change_callback_ =
base::Bind(&ToolbarActionsModel::OnActionToolbarPrefChange,
base::Unretained(this));
pref_change_registrar_.Add(extensions::pref_names::kToolbar,
pref_change_callback_);
}
}
ToolbarActionsModel::~ToolbarActionsModel() {}
// static
ToolbarActionsModel* ToolbarActionsModel::Get(Profile* profile) {
return ToolbarActionsModelFactory::GetForProfile(profile);
}
void ToolbarActionsModel::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void ToolbarActionsModel::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void ToolbarActionsModel::MoveActionIcon(const std::string& id, size_t index) {
std::vector<ToolbarItem>::iterator pos = toolbar_items_.begin();
while (pos != toolbar_items_.end() && (*pos).id != id)
++pos;
if (pos == toolbar_items_.end()) {
NOTREACHED();
return;
}
ToolbarItem action = *pos;
toolbar_items_.erase(pos);
std::vector<std::string>::iterator pos_id =
std::find(last_known_positions_.begin(), last_known_positions_.end(), id);
if (pos_id != last_known_positions_.end())
last_known_positions_.erase(pos_id);
if (index < toolbar_items_.size()) {
// If the index is not at the end, find the item currently at |index|, and
// insert |action| before it in |toolbar_items_| and |action|'s id in
// |last_known_positions_|.
std::vector<ToolbarItem>::iterator iter = toolbar_items_.begin() + index;
last_known_positions_.insert(
std::find(last_known_positions_.begin(), last_known_positions_.end(),
iter->id),
id);
toolbar_items_.insert(iter, action);
} else {
// Otherwise, put |action| and |id| at the end.
DCHECK_EQ(toolbar_items_.size(), index);
toolbar_items_.push_back(action);
last_known_positions_.push_back(id);
}
for (Observer& observer : observers_)
observer.OnToolbarActionMoved(id, index);
UpdatePrefs();
}
void ToolbarActionsModel::SetVisibleIconCount(size_t count) {
visible_icon_count_ = (count >= toolbar_items_.size()) ? -1 : count;
// Only set the prefs if we're not in highlight mode and the profile is not
// incognito. Highlight mode is designed to be a transitory state, and should
// not persist across browser restarts (though it may be re-entered), and we
// don't store anything in incognito.
if (!is_highlighting() && !profile_->IsOffTheRecord()) {
prefs_->SetInteger(extensions::pref_names::kToolbarSize,
visible_icon_count_);
}
for (Observer& observer : observers_)
observer.OnToolbarVisibleCountChanged();
}
void ToolbarActionsModel::OnExtensionActionUpdated(
ExtensionAction* extension_action,
content::WebContents* web_contents,
content::BrowserContext* browser_context) {
// Notify observers if the extension exists and is in the model.
if (HasItem(
ToolbarItem(extension_action->extension_id(), EXTENSION_ACTION))) {
for (Observer& observer : observers_)
observer.OnToolbarActionUpdated(extension_action->extension_id());
}
}
std::vector<std::unique_ptr<ToolbarActionViewController>>
ToolbarActionsModel::CreateActions(Browser* browser, ToolbarActionsBar* bar) {
DCHECK(browser);
DCHECK(bar);
std::vector<std::unique_ptr<ToolbarActionViewController>> action_list;
// toolbar_items() might not equate to toolbar_items_ in the case where a
// subset is highlighted.
for (const ToolbarItem& item : toolbar_items())
action_list.push_back(CreateActionForItem(browser, bar, item));
return action_list;
}
std::unique_ptr<ToolbarActionViewController>
ToolbarActionsModel::CreateActionForItem(Browser* browser,
ToolbarActionsBar* bar,
const ToolbarItem& item) {
std::unique_ptr<ToolbarActionViewController> result;
switch (item.type) {
case EXTENSION_ACTION: {
// Get the extension.
const extensions::Extension* extension = GetExtensionById(item.id);
DCHECK(extension);
// Create and add an ExtensionActionViewController for the extension.
result = base::MakeUnique<ExtensionActionViewController>(
extension, browser,
extension_action_manager_->GetExtensionAction(*extension), bar);
break;
}
case COMPONENT_ACTION: {
result = component_actions_factory_->GetComponentToolbarActionForId(
item.id, browser, bar);
break;
}
case UNKNOWN_ACTION:
NOTREACHED(); // Should never have an UNKNOWN_ACTION in toolbar_items.
break;
}
return result;
}
void ToolbarActionsModel::OnExtensionLoaded(
content::BrowserContext* browser_context,
const extensions::Extension* extension) {
// We don't want to add the same extension twice. It may have already been
// added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user
// hides the browser action and then disables and enables the extension.
if (!HasItem(ToolbarItem(extension->id(), EXTENSION_ACTION)))
AddExtension(extension);
}
void ToolbarActionsModel::OnExtensionUnloaded(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UnloadedExtensionReason reason) {
bool was_visible_and_has_overflow =
IsActionVisible(extension->id()) && !all_icons_visible();
RemoveExtension(extension);
// If the extension was previously visible and there are overflowed
// extensions, and this extension is being uninstalled, we reduce the visible
// count so that we don't pop out a previously-hidden extension.
if (was_visible_and_has_overflow &&
reason == extensions::UnloadedExtensionReason::UNINSTALL)
SetVisibleIconCount(visible_icon_count() - 1);
}
void ToolbarActionsModel::OnExtensionUninstalled(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UninstallReason reason) {
// Remove the extension id from the ordered list, if it exists (the extension
// might not be represented in the list because it might not have an icon).
RemovePref(ToolbarItem(extension->id(), EXTENSION_ACTION));
}
void ToolbarActionsModel::RemovePref(const ToolbarItem& item) {
std::vector<std::string>::iterator pos = std::find(
last_known_positions_.begin(), last_known_positions_.end(), item.id);
if (pos != last_known_positions_.end()) {
last_known_positions_.erase(pos);
UpdatePrefs();
}
}
void ToolbarActionsModel::OnReady() {
InitializeActionList();
// Wait until the extension system is ready before observing any further
// changes so that the toolbar buttons can be shown in their stable ordering
// taken from prefs.
extension_registry_observer_.Add(extension_registry_);
extension_action_observer_.Add(extension_action_api_);
actions_initialized_ = true;
for (Observer& observer : observers_)
observer.OnToolbarModelInitialized();
}
size_t ToolbarActionsModel::FindNewPositionFromLastKnownGood(
const ToolbarItem& action) {
// See if we have last known good position for this action.
size_t new_index = 0;
// Loop through the ID list of known positions, to count the number of
// visible action icons preceding |action|'s id.
for (const std::string& last_pos_id : last_known_positions_) {
if (last_pos_id == action.id)
return new_index; // We've found the right position.
// Found an action, need to see if it is visible.
for (const ToolbarItem& item : toolbar_items_) {
if (item.id == last_pos_id) {
// This extension is visible, update the index value.
++new_index;
break;
}
}
}
// Position not found.
return toolbar_items_.size();
}
bool ToolbarActionsModel::ShouldAddExtension(
const extensions::Extension* extension) {
// In incognito mode, don't add any extensions that aren't incognito-enabled.
if (profile_->IsOffTheRecord() &&
!extensions::util::IsIncognitoEnabled(extension->id(), profile_))
return false;
// In this case, we don't care about the browser action visibility, because
// we want to show each extension regardless.
return extension_action_manager_->GetExtensionAction(*extension) != nullptr;
}
void ToolbarActionsModel::AddExtension(const extensions::Extension* extension) {
if (!ShouldAddExtension(extension))
return;
AddItem(ToolbarItem(extension->id(), EXTENSION_ACTION));
}
void ToolbarActionsModel::AddItem(const ToolbarItem& item) {
// We only use AddItem() once the system is initialized.
CHECK(actions_initialized_);
// See if we have a last known good position for this extension.
bool is_new_extension = !base::ContainsValue(last_known_positions_, item.id);
// New extensions go at the right (end) of the visible extensions. Other
// extensions go at their previous position.
size_t new_index = 0;
if (is_new_extension) {
new_index = visible_icon_count();
// For the last-known position, we use the index of the extension that is
// just before this extension, plus one. (Note that this isn't the same
// as new_index + 1, because last_known_positions_ can include disabled
// extensions.)
int new_last_known_index =
new_index == 0 ? 0 : std::find(last_known_positions_.begin(),
last_known_positions_.end(),
toolbar_items_[new_index - 1].id) -
last_known_positions_.begin() + 1;
// In theory, the extension before this one should always
// be in last known positions, but if something funny happened with prefs,
// make sure we handle it.
// TODO(devlin): Track down these cases so we can CHECK this.
new_last_known_index =
std::min<int>(new_last_known_index, last_known_positions_.size());
last_known_positions_.insert(
last_known_positions_.begin() + new_last_known_index, item.id);
UpdatePrefs();
} else {
new_index = FindNewPositionFromLastKnownGood(item);
}
toolbar_items_.insert(toolbar_items_.begin() + new_index, item);
// If we're currently highlighting, then even though we add a browser action
// to the full list (|toolbar_items_|, there won't be another *visible*
// browser action, which was what the observers care about.
if (!is_highlighting()) {
for (Observer& observer : observers_)
observer.OnToolbarActionAdded(item, new_index);
int visible_count_delta = 0;
if (is_new_extension && !all_icons_visible()) {
// If this is a new extension (and not all extensions are visible), we
// expand the toolbar out so that the new one can be seen.
visible_count_delta = 1;
} else if (profile_->IsOffTheRecord()) {
// If this is an incognito profile, we also have to check to make sure the
// overflow matches the main bar's status.
ToolbarActionsModel* main_model =
ToolbarActionsModel::Get(profile_->GetOriginalProfile());
// Find what the index will be in the main bar. Because Observer calls are
// nondeterministic, we can't just assume the main bar will have the
// extension and look it up.
size_t main_index = main_model->FindNewPositionFromLastKnownGood(item);
bool visible =
is_new_extension || main_index < main_model->visible_icon_count();
// We may need to adjust the visible count if the incognito bar isn't
// showing all icons and this one is visible, or if it is showing all
// icons and this is hidden.
if (visible && !all_icons_visible())
visible_count_delta = 1;
else if (!visible && all_icons_visible())
visible_count_delta = -1;
}
if (visible_count_delta)
SetVisibleIconCount(visible_icon_count() + visible_count_delta);
}
}
void ToolbarActionsModel::RemoveItem(const ToolbarItem& item) {
std::vector<ToolbarItem>::iterator pos =
std::find(toolbar_items_.begin(), toolbar_items_.end(), item);
if (pos == toolbar_items_.end())
return;
// If our visible count is set to the current size, we need to decrement it.
if (visible_icon_count_ == static_cast<int>(toolbar_items_.size()))
SetVisibleIconCount(toolbar_items_.size() - 1);
toolbar_items_.erase(pos);
// If we're in highlight mode, we also have to remove the action from
// the highlighted list.
if (is_highlighting()) {
pos = std::find(highlighted_items_.begin(), highlighted_items_.end(), item);
if (pos != highlighted_items_.end()) {
highlighted_items_.erase(pos);
for (Observer& observer : observers_)
observer.OnToolbarActionRemoved(item.id);
// If the highlighted list is now empty, we stop highlighting.
if (highlighted_items_.empty())
StopHighlighting();
}
} else {
for (Observer& observer : observers_)
observer.OnToolbarActionRemoved(item.id);
}
UpdatePrefs();
}
std::unique_ptr<extensions::ExtensionMessageBubbleController>
ToolbarActionsModel::GetExtensionMessageBubbleController(Browser* browser) {
std::unique_ptr<extensions::ExtensionMessageBubbleController> controller;
if (has_active_bubble())
return controller;
controller = ExtensionMessageBubbleFactory(browser).GetController();
if (controller)
controller->SetIsActiveBubble();
return controller;
}
void ToolbarActionsModel::SetMockActionsFactoryForTest(
std::unique_ptr<ComponentToolbarActionsFactory> mock_factory) {
component_actions_factory_ = std::move(mock_factory);
}
void ToolbarActionsModel::RemoveExtension(
const extensions::Extension* extension) {
RemoveItem(ToolbarItem(extension->id(), EXTENSION_ACTION));
}
// Combine the currently enabled extensions that have browser actions (which
// we get from the ExtensionRegistry) and component actions (which we get from
// ComponentToolbarActionsFactory) with the ordering we get from the pref
// service. For robustness we use a somewhat inefficient process:
// 1. Create a vector of actions sorted by their pref values. This vector may
// have holes.
// 2. Create a vector of actions that did not have a pref value.
// 3. Remove holes from the sorted vector and append the unsorted vector.
void ToolbarActionsModel::InitializeActionList() {
CHECK(toolbar_items_.empty()); // We shouldn't have any items yet.
last_known_positions_ = extension_prefs_->GetToolbarOrder();
if (profile_->IsOffTheRecord())
IncognitoPopulate();
else
Populate();
}
void ToolbarActionsModel::Populate() {
DCHECK(!profile_->IsOffTheRecord());
std::vector<ToolbarItem> all_actions;
// Ids of actions that have explicit positions.
std::vector<ToolbarItem> sorted(last_known_positions_.size(), ToolbarItem());
// Ids of actions that don't have explicit positions.
std::vector<ToolbarItem> unsorted;
// Populate the lists.
int hidden = 0;
int browser_actions_count = 0;
int component_actions_count = 0;
// First, add the extension action ids to all_actions.
const extensions::ExtensionSet& extensions =
extension_registry_->enabled_extensions();
for (const scoped_refptr<const extensions::Extension>& extension :
extensions) {
if (!ShouldAddExtension(extension.get())) {
if (!extension_action_api_->GetBrowserActionVisibility(extension->id()))
++hidden;
continue;
}
all_actions.push_back(ToolbarItem(extension->id(), EXTENSION_ACTION));
}
// Next, add the component action ids.
std::set<std::string> component_ids =
component_actions_factory_->GetInitialComponentIds();
for (const std::string& id : component_ids)
all_actions.push_back(ToolbarItem(id, COMPONENT_ACTION));
// Add each action id to the appropriate list. Since the |sorted| list is
// created with enough room for each id in |positions| (which helps with
// proper order insertion), holes can be present if there isn't an action
// for each id. This is handled below when we add the actions to
// |toolbar_items_| to ensure that there are never any holes in
// |toolbar_items_| itself (or, relatedly, CreateActions()).
for (const ToolbarItem& action : all_actions) {
std::vector<std::string>::const_iterator pos =
std::find(last_known_positions_.begin(), last_known_positions_.end(),
action.id);
if (pos != last_known_positions_.end()) {
sorted[pos - last_known_positions_.begin()] = action;
} else {
// Unknown action - push it to the back of unsorted, and add it to the
// list of ids at the end.
unsorted.push_back(action);
last_known_positions_.push_back(action.id);
}
}
// Merge the lists.
sorted.insert(sorted.end(), unsorted.begin(), unsorted.end());
toolbar_items_.reserve(sorted.size());
// We don't notify observers of the added extension yet. Rather, observers
// should wait for the "OnToolbarModelInitialized" notification, and then
// bulk-update. (This saves a lot of bouncing-back-and-forth here, and allows
// observers to ensure that the extension system is always initialized before
// using the extensions).
for (const ToolbarItem& action : sorted) {
switch (action.type) {
case EXTENSION_ACTION:
// It's possible for the extension order to contain items that aren't
// actually loaded on this machine. For example, when extension sync is
// on, we sync the extension order as-is but double-check with the user
// before syncing NPAPI-containing extensions, so if one of those is not
// actually synced, we'll get a NULL in the list. This sort of case can
// also happen if some error prevents an extension from loading.
if (GetExtensionById(action.id)) {
toolbar_items_.push_back(ToolbarItem(action.id, EXTENSION_ACTION));
++browser_actions_count;
}
break;
case COMPONENT_ACTION:
toolbar_items_.push_back(ToolbarItem(action.id, COMPONENT_ACTION));
++component_actions_count;
break;
case UNKNOWN_ACTION:
// Since |sorted| can have holes in it, they will be default-constructed
// ToolbarItems with an action type of UNKNOWN. Ignore them.
break;
}
}
// Histogram names are prefixed with "ExtensionToolbarModel" rather than
// "ToolbarActionsModel" for historical reasons.
UMA_HISTOGRAM_COUNTS_100(
"ExtensionToolbarModel.BrowserActionsPermanentlyHidden", hidden);
UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsCount",
browser_actions_count);
UMA_HISTOGRAM_COUNTS_100("Toolbar.ActionsModel.ComponentActionsCount",
component_actions_count);
UMA_HISTOGRAM_COUNTS_100("Toolbar.ActionsModel.OverallActionsCount",
toolbar_items_.size());
const char kDocsOfflineExtensionId[] = "ghbmnnjooekpmoecnnnilnnbdlolhkhi";
if (extension_registry_->GetExtensionById(
kDocsOfflineExtensionId,
extensions::ExtensionRegistry::ENABLED |
extensions::ExtensionRegistry::DISABLED) != nullptr) {
// Note: This enum is used in UMA (directly below). Don't renumber.
enum ExtensionState {
DISABLED = 0,
VISIBLE = 1,
OVERFLOWED = 2,
BOUNDARY = 3,
};
ExtensionState doc_state = DISABLED;
if (extensions.GetByID(kDocsOfflineExtensionId)) { // In the enabled set.
auto current_pos = std::find_if(
toolbar_items_.begin(), toolbar_items_.end(),
[&kDocsOfflineExtensionId](const ToolbarItem& item) {
return item.id == kDocsOfflineExtensionId;
});
doc_state =
current_pos - toolbar_items_.begin() <
static_cast<int>(visible_icon_count()) ||
all_icons_visible() ?
VISIBLE : OVERFLOWED;
}
UMA_HISTOGRAM_ENUMERATION("Extensions.DocsOfflineIconState",
doc_state, BOUNDARY);
}
if (!toolbar_items_.empty()) {
// Visible count can be -1, meaning: 'show all'. Since UMA converts negative
// values to 0, this would be counted as 'show none' unless we convert it to
// max.
UMA_HISTOGRAM_COUNTS_100(
"ExtensionToolbarModel.BrowserActionsVisible",
visible_icon_count_ == -1
? base::HistogramBase::kSampleType_MAX
: visible_icon_count_ - component_actions_count);
UMA_HISTOGRAM_COUNTS_100("Toolbar.ActionsModel.ToolbarActionsVisible",
visible_icon_count_ == -1
? base::HistogramBase::kSampleType_MAX
: visible_icon_count_);
}
}
bool ToolbarActionsModel::HasItem(const ToolbarItem& item) const {
return base::ContainsValue(toolbar_items_, item);
}
bool ToolbarActionsModel::HasComponentAction(
const std::string& action_id) const {
return HasItem(ToolbarItem(action_id, COMPONENT_ACTION));
}
void ToolbarActionsModel::AddComponentAction(const std::string& action_id) {
if (!actions_initialized_) {
component_actions_factory_->OnAddComponentActionBeforeInit(action_id);
return;
}
ToolbarItem component_item(action_id, COMPONENT_ACTION);
DCHECK(!HasItem(component_item));
AddItem(component_item);
}
void ToolbarActionsModel::RemoveComponentAction(const std::string& action_id) {
if (!actions_initialized_) {
component_actions_factory_->OnRemoveComponentActionBeforeInit(action_id);
return;
}
// If the action was visible and there are overflowed actions, we reduce the
// visible count so that we don't pop out a previously-hidden action.
if (IsActionVisible(action_id) && !all_icons_visible())
SetVisibleIconCount(visible_icon_count() - 1);
ToolbarItem component_item(action_id, COMPONENT_ACTION);
DCHECK(HasItem(component_item));
RemoveItem(component_item);
RemovePref(component_item);
}
void ToolbarActionsModel::IncognitoPopulate() {
DCHECK(profile_->IsOffTheRecord());
const ToolbarActionsModel* original_model =
ToolbarActionsModel::Get(profile_->GetOriginalProfile());
// Find the absolute value of the original model's count.
int original_visible = original_model->visible_icon_count();
// In incognito mode, we show only those actions that are incognito-enabled
// Further, any actions that were overflowed in regular mode are still
// overflowed. Order is the same as in regular mode.
visible_icon_count_ = 0;
std::set<std::string> component_ids =
component_actions_factory_->GetInitialComponentIds();
for (std::vector<ToolbarItem>::const_iterator iter =
original_model->toolbar_items_.begin();
iter != original_model->toolbar_items_.end(); ++iter) {
// The extension might not be shown in incognito mode.
// We may also disable certain component actions in incognito mode.
bool should_add = false;
switch (iter->type) {
case EXTENSION_ACTION:
should_add = ShouldAddExtension(GetExtensionById(iter->id));
break;
case COMPONENT_ACTION:
// The component action factory only returns actions that should be
// added.
should_add = component_ids.count(iter->id) != 0;
break;
case UNKNOWN_ACTION:
// We should never have an uninitialized action in the model.
NOTREACHED();
break;
}
if (!should_add)
continue;
toolbar_items_.push_back(*iter);
if (iter - original_model->toolbar_items_.begin() < original_visible)
++visible_icon_count_;
}
}
void ToolbarActionsModel::UpdatePrefs() {
if (!extension_prefs_ || profile_->IsOffTheRecord())
return;
// Don't observe change caused by self.
pref_change_registrar_.Remove(extensions::pref_names::kToolbar);
extension_prefs_->SetToolbarOrder(last_known_positions_);
pref_change_registrar_.Add(extensions::pref_names::kToolbar,
pref_change_callback_);
}
void ToolbarActionsModel::SetActionVisibility(const std::string& action_id,
bool is_now_visible) {
// Hiding works differently with the new and old toolbars.
DCHECK(HasItem(ToolbarItem(action_id, EXTENSION_ACTION)));
int new_size = 0;
int new_index = 0;
if (is_now_visible) {
// If this action used to be hidden, we can't possibly be showing all.
DCHECK_LT(visible_icon_count(), toolbar_items_.size());
// Grow the bar by one and move the action to the end of the visibles.
new_size = visible_icon_count() + 1;
new_index = new_size - 1;
} else {
// If we're hiding one, we must be showing at least one.
DCHECK_GE(visible_icon_count(), 0u);
// Shrink the bar by one and move the action to the beginning of the
// overflow menu.
new_size = visible_icon_count() - 1;
new_index = new_size;
}
SetVisibleIconCount(new_size);
MoveActionIcon(action_id, new_index);
}
void ToolbarActionsModel::OnActionToolbarPrefChange() {
// If extensions are not ready, defer to later Populate() call.
if (!actions_initialized_)
return;
// Recalculate |last_known_positions_| to be |pref_positions| followed by
// ones that are only in |last_known_positions_|.
std::vector<std::string> pref_positions = extension_prefs_->GetToolbarOrder();
size_t pref_position_size = pref_positions.size();
for (size_t i = 0; i < last_known_positions_.size(); ++i) {
if (!base::ContainsValue(pref_positions, last_known_positions_[i])) {
pref_positions.push_back(last_known_positions_[i]);
}
}
last_known_positions_.swap(pref_positions);
// Loop over the updated list of last known positions, moving any extensions
// that are in the wrong place.
auto desired_pos = toolbar_items_.begin();
for (const std::string& id : last_known_positions_) {
auto current_pos = std::find_if(
toolbar_items_.begin(), toolbar_items_.end(),
[&id](const ToolbarItem& item) { return item.id == id; });
if (current_pos == toolbar_items_.end())
continue;
if (current_pos != desired_pos) {
if (current_pos < desired_pos)
std::rotate(current_pos, current_pos + 1, desired_pos + 1);
else
std::rotate(desired_pos, current_pos, current_pos + 1);
// Notify the observers to keep them up to date, unless we're highlighting
// (in which case we're deliberately only showing a subset of actions).
if (!is_highlighting()) {
for (Observer& observer : observers_) {
observer.OnToolbarActionMoved(id,
desired_pos - toolbar_items_.begin());
}
}
}
++desired_pos;
}
if (last_known_positions_.size() > pref_position_size) {
// Need to update pref because we have extra icons. But can't call
// UpdatePrefs() directly within observation closure.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&ToolbarActionsModel::UpdatePrefs,
weak_ptr_factory_.GetWeakPtr()));
}
}
bool ToolbarActionsModel::HighlightActions(const std::vector<std::string>& ids,
HighlightType highlight_type) {
highlighted_items_.clear();
for (const std::string& action_id : ids) {
for (const ToolbarItem& item : toolbar_items_) {
if (action_id == item.id)
highlighted_items_.push_back(item);
}
}
// If we have any items in |highlighted_items_|, then we entered highlighting
// mode.
if (highlighted_items_.size()) {
// It's important that |highlight_type_| is changed immediately before the
// observers are notified since it changes the result of toolbar_items().
highlight_type_ = highlight_type;
for (Observer& observer : observers_)
observer.OnToolbarHighlightModeChanged(true);
// We set the visible icon count after the highlight mode change because
// the UI actions are created/destroyed during highlight, and doing that
// prior to changing the size allows us to still have smooth animations.
if (visible_icon_count() < ids.size())
SetVisibleIconCount(ids.size());
return true;
}
// Otherwise, we didn't enter highlighting mode (and, in fact, exited it if
// we were otherwise in it).
if (is_highlighting())
StopHighlighting();
return false;
}
void ToolbarActionsModel::StopHighlighting() {
if (is_highlighting()) {
// It's important that |highlight_type_| is changed immediately before the
// observers are notified since it changes the result of toolbar_items().
highlight_type_ = HIGHLIGHT_NONE;
for (Observer& observer : observers_)
observer.OnToolbarHighlightModeChanged(false);
// For the same reason, we don't clear highlighted_items_ until after the
// mode changed.
highlighted_items_.clear();
// We set the visible icon count after the highlight mode change because
// the UI actions are created/destroyed during highlight, and doing that
// prior to changing the size allows us to still have smooth animations.
int saved_icon_count =
prefs_->GetInteger(extensions::pref_names::kToolbarSize);
if (saved_icon_count != visible_icon_count_)
SetVisibleIconCount(saved_icon_count);
}
}
const extensions::Extension* ToolbarActionsModel::GetExtensionById(
const std::string& id) const {
return extension_registry_->enabled_extensions().GetByID(id);
}
bool ToolbarActionsModel::IsActionVisible(const std::string& action_id) const {
size_t index = 0u;
while (toolbar_items().size() > index &&
toolbar_items()[index].id != action_id)
++index;
return index < visible_icon_count();
}