blob: 50fb694637c18442d72ce12516f2ff581e83706d [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/zoom/zoom_controller.h"
#include "chrome/browser/ui/sad_tab.h"
#include "chrome/browser/ui/zoom/zoom_event_manager.h"
#include "chrome/browser/ui/zoom/zoom_observer.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/page_type.h"
#include "content/public/common/page_zoom.h"
#include "extensions/common/extension.h"
#include "grit/theme_resources.h"
#include "net/base/net_util.h"
DEFINE_WEB_CONTENTS_USER_DATA_KEY(ZoomController);
ZoomController::ZoomController(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
can_show_bubble_(true),
zoom_mode_(ZOOM_MODE_DEFAULT),
zoom_level_(1.0),
browser_context_(web_contents->GetBrowserContext()) {
// TODO(wjmaclean) Make calls to HostZoomMap::GetDefaultForBrowserContext()
// refer to the webcontents-specific HostZoomMap when that becomes available.
content::HostZoomMap* host_zoom_map =
content::HostZoomMap::GetDefaultForBrowserContext(browser_context_);
zoom_level_ = host_zoom_map->GetDefaultZoomLevel();
zoom_subscription_ = host_zoom_map->AddZoomLevelChangedCallback(
base::Bind(&ZoomController::OnZoomLevelChanged, base::Unretained(this)));
UpdateState(std::string());
}
ZoomController::~ZoomController() {}
bool ZoomController::IsAtDefaultZoom() const {
return content::ZoomValuesEqual(GetZoomLevel(), GetDefaultZoomLevel());
}
int ZoomController::GetResourceForZoomLevel() const {
if (IsAtDefaultZoom())
return IDR_ZOOM_NORMAL;
return GetZoomLevel() > GetDefaultZoomLevel() ? IDR_ZOOM_PLUS
: IDR_ZOOM_MINUS;
}
void ZoomController::AddObserver(ZoomObserver* observer) {
observers_.AddObserver(observer);
}
void ZoomController::RemoveObserver(ZoomObserver* observer) {
observers_.RemoveObserver(observer);
}
double ZoomController::GetZoomLevel() const {
return zoom_mode_ == ZOOM_MODE_MANUAL ?
zoom_level_:
content::HostZoomMap::GetZoomLevel(web_contents());
}
int ZoomController::GetZoomPercent() const {
double zoom_factor = content::ZoomLevelToZoomFactor(GetZoomLevel());
// Round double for return.
return static_cast<int>(zoom_factor * 100 + 0.5);
}
bool ZoomController::SetZoomLevel(double zoom_level) {
// An extension did not initiate this zoom change.
return SetZoomLevelByExtension(zoom_level, NULL);
}
bool ZoomController::SetZoomLevelByExtension(
double zoom_level,
const scoped_refptr<const extensions::Extension>& extension) {
content::NavigationEntry* entry =
web_contents()->GetController().GetLastCommittedEntry();
bool is_normal_page =
entry && entry->GetPageType() == content::PAGE_TYPE_NORMAL;
// Cannot zoom in disabled mode. Also, don't allow changing zoom level on
// a crashed tab, an error page or an interstitial page.
if (zoom_mode_ == ZOOM_MODE_DISABLED ||
!web_contents()->GetRenderViewHost()->IsRenderViewLive() ||
!is_normal_page)
return false;
// Store extension data so that |extension| can be attributed when the zoom
// change completes. We expect that by the time this function returns that
// any observers that require this information will have requested it.
last_extension_ = extension;
// Do not actually rescale the page in manual mode.
if (zoom_mode_ == ZOOM_MODE_MANUAL) {
double old_zoom_level = zoom_level_;
zoom_level_ = zoom_level;
// TODO(wjmaclean) Do we care about filling in host/scheme here?
content::HostZoomMap::ZoomLevelChange change;
change.mode = content::HostZoomMap::ZOOM_CHANGED_TEMPORARY_ZOOM;
change.zoom_level = zoom_level;
ZoomEventManager::GetForBrowserContext(browser_context_)->
OnZoomLevelChanged(change);
ZoomChangedEventData zoom_change_data(web_contents(),
old_zoom_level,
zoom_level_,
zoom_mode_,
false /* can_show_bubble */);
FOR_EACH_OBSERVER(
ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
last_extension_ = NULL;
return true;
}
content::HostZoomMap* zoom_map =
content::HostZoomMap::GetDefaultForBrowserContext(browser_context_);
DCHECK(zoom_map);
DCHECK(!event_data_);
event_data_.reset(new ZoomChangedEventData(web_contents(),
GetZoomLevel(),
zoom_level,
zoom_mode_,
false /* can_show_bubble */));
int render_process_id = web_contents()->GetRenderProcessHost()->GetID();
int render_view_id = web_contents()->GetRenderViewHost()->GetRoutingID();
if (zoom_mode_ == ZOOM_MODE_ISOLATED ||
zoom_map->UsesTemporaryZoomLevel(render_process_id, render_view_id)) {
zoom_map->SetTemporaryZoomLevel(
render_process_id, render_view_id, zoom_level);
} else {
if (!entry) {
last_extension_ = NULL;
return false;
}
std::string host = net::GetHostOrSpecFromURL(entry->GetURL());
zoom_map->SetZoomLevelForHost(host, zoom_level);
}
DCHECK(!event_data_);
last_extension_ = NULL;
return true;
}
void ZoomController::SetZoomMode(ZoomMode new_mode) {
if (new_mode == zoom_mode_)
return;
content::HostZoomMap* zoom_map =
content::HostZoomMap::GetDefaultForBrowserContext(browser_context_);
DCHECK(zoom_map);
int render_process_id = web_contents()->GetRenderProcessHost()->GetID();
int render_view_id = web_contents()->GetRenderViewHost()->GetRoutingID();
double original_zoom_level = GetZoomLevel();
DCHECK(!event_data_);
event_data_.reset(new ZoomChangedEventData(web_contents(),
original_zoom_level,
original_zoom_level,
new_mode,
new_mode != ZOOM_MODE_DEFAULT));
switch (new_mode) {
case ZOOM_MODE_DEFAULT: {
content::NavigationEntry* entry =
web_contents()->GetController().GetLastCommittedEntry();
if (entry) {
GURL url = entry->GetURL();
std::string host = net::GetHostOrSpecFromURL(url);
if (zoom_map->HasZoomLevel(url.scheme(), host)) {
// If there are other tabs with the same origin, then set this tab's
// zoom level to match theirs. The temporary zoom level will be
// cleared below, but this call will make sure this tab re-draws at
// the correct zoom level.
double origin_zoom_level =
zoom_map->GetZoomLevelForHostAndScheme(url.scheme(), host);
event_data_->new_zoom_level = origin_zoom_level;
zoom_map->SetTemporaryZoomLevel(
render_process_id, render_view_id, origin_zoom_level);
} else {
// The host will need a level prior to removing the temporary level.
// We don't want the zoom level to change just because we entered
// default mode.
zoom_map->SetZoomLevelForHost(host, original_zoom_level);
}
}
// Remove per-tab zoom data for this tab. No event callback expected.
zoom_map->ClearTemporaryZoomLevel(render_process_id, render_view_id);
break;
}
case ZOOM_MODE_ISOLATED: {
// Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
// page needs an initial isolated zoom back to the same level it was at
// in the other mode.
if (zoom_mode_ != ZOOM_MODE_DISABLED) {
zoom_map->SetTemporaryZoomLevel(
render_process_id, render_view_id, original_zoom_level);
} else {
// When we don't call any HostZoomMap set functions, we send the event
// manually.
FOR_EACH_OBSERVER(
ZoomObserver, observers_, OnZoomChanged(*event_data_));
event_data_.reset();
}
break;
}
case ZOOM_MODE_MANUAL: {
// Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
// page needs to be resized to the default zoom. While in manual mode,
// the zoom level is handled independently.
if (zoom_mode_ != ZOOM_MODE_DISABLED) {
zoom_map->SetTemporaryZoomLevel(
render_process_id, render_view_id, GetDefaultZoomLevel());
zoom_level_ = original_zoom_level;
} else {
// When we don't call any HostZoomMap set functions, we send the event
// manually.
FOR_EACH_OBSERVER(
ZoomObserver, observers_, OnZoomChanged(*event_data_));
event_data_.reset();
}
break;
}
case ZOOM_MODE_DISABLED: {
// The page needs to be zoomed back to default before disabling the zoom
zoom_map->SetTemporaryZoomLevel(
render_process_id, render_view_id, GetDefaultZoomLevel());
break;
}
}
// Any event data we've stored should have been consumed by this point.
DCHECK(!event_data_);
zoom_mode_ = new_mode;
}
void ZoomController::DidNavigateMainFrame(
const content::LoadCommittedDetails& details,
const content::FrameNavigateParams& params) {
// If the main frame's content has changed, the new page may have a different
// zoom level from the old one.
UpdateState(std::string());
}
void ZoomController::WebContentsDestroyed() {
// At this point we should no longer be sending any zoom events with this
// WebContents.
observers_.Clear();
}
void ZoomController::OnZoomLevelChanged(
const content::HostZoomMap::ZoomLevelChange& change) {
UpdateState(change.host);
}
void ZoomController::UpdateState(const std::string& host) {
// If |host| is empty, all observers should be updated.
if (!host.empty()) {
// Use the navigation entry's URL instead of the WebContents' so virtual
// URLs work (e.g. chrome://settings). http://crbug.com/153950
content::NavigationEntry* entry =
web_contents()->GetController().GetLastCommittedEntry();
if (!entry ||
host != net::GetHostOrSpecFromURL(entry->GetURL())) {
return;
}
}
// The zoom bubble should not be shown for zoom changes where the host is
// empty.
bool can_show_bubble = can_show_bubble_ && !host.empty();
if (event_data_) {
// For state changes initiated within the ZoomController, information about
// the change should be sent.
ZoomChangedEventData zoom_change_data = *event_data_;
event_data_.reset();
zoom_change_data.can_show_bubble = can_show_bubble;
FOR_EACH_OBSERVER(
ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
} else {
// TODO(wjmaclean) Should we consider having HostZoomMap send both old and
// new zoom levels here?
double zoom_level = GetZoomLevel();
ZoomChangedEventData zoom_change_data(
web_contents(), zoom_level, zoom_level, zoom_mode_, can_show_bubble);
FOR_EACH_OBSERVER(
ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
}
}