1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/zoom/zoom_controller.h"
7 #include "base/prefs/pref_service.h"
8 #include "chrome/browser/chrome_notification_types.h"
9 #include "chrome/browser/profiles/profile.h"
10 #include "chrome/browser/ui/browser_finder.h"
11 #include "chrome/browser/ui/zoom/zoom_event_manager.h"
12 #include "chrome/common/pref_names.h"
13 #include "content/public/browser/host_zoom_map.h"
14 #include "content/public/browser/navigation_entry.h"
15 #include "content/public/browser/notification_details.h"
16 #include "content/public/browser/notification_service.h"
17 #include "content/public/browser/notification_types.h"
18 #include "content/public/browser/render_process_host.h"
19 #include "content/public/browser/render_view_host.h"
20 #include "content/public/browser/web_contents.h"
21 #include "content/public/common/page_zoom.h"
22 #include "extensions/common/extension.h"
23 #include "grit/theme_resources.h"
24 #include "net/base/net_util.h"
26 DEFINE_WEB_CONTENTS_USER_DATA_KEY(ZoomController);
28 ZoomController::ZoomController(content::WebContents* web_contents)
29 : content::WebContentsObserver(web_contents),
30 can_show_bubble_(true),
31 zoom_mode_(ZOOM_MODE_DEFAULT),
33 browser_context_(web_contents->GetBrowserContext()) {
35 Profile::FromBrowserContext(web_contents->GetBrowserContext());
36 default_zoom_level_.Init(
37 prefs::kDefaultZoomLevel,
40 &ZoomController::UpdateState, base::Unretained(this), std::string()));
41 zoom_level_ = default_zoom_level_.GetValue();
43 zoom_subscription_ = content::HostZoomMap::GetForBrowserContext(
44 browser_context_)->AddZoomLevelChangedCallback(
45 base::Bind(&ZoomController::OnZoomLevelChanged,
46 base::Unretained(this)));
48 UpdateState(std::string());
51 ZoomController::~ZoomController() {}
53 bool ZoomController::IsAtDefaultZoom() const {
54 return content::ZoomValuesEqual(GetZoomLevel(),
55 default_zoom_level_.GetValue());
58 int ZoomController::GetResourceForZoomLevel() const {
59 if (IsAtDefaultZoom())
60 return IDR_ZOOM_NORMAL;
61 return GetZoomLevel() > default_zoom_level_.GetValue() ? IDR_ZOOM_PLUS
65 void ZoomController::AddObserver(ZoomObserver* observer) {
66 observers_.AddObserver(observer);
69 void ZoomController::RemoveObserver(ZoomObserver* observer) {
70 observers_.RemoveObserver(observer);
73 double ZoomController::GetZoomLevel() const {
74 return zoom_mode_ == ZOOM_MODE_MANUAL ?
76 content::HostZoomMap::GetZoomLevel(web_contents());
79 int ZoomController::GetZoomPercent() const {
80 double zoom_factor = content::ZoomLevelToZoomFactor(GetZoomLevel());
81 // Round double for return.
82 return static_cast<int>(zoom_factor * 100 + 0.5);
85 bool ZoomController::SetZoomLevel(double zoom_level) {
86 // An extension did not initiate this zoom change.
87 return SetZoomLevelByExtension(zoom_level, NULL);
90 bool ZoomController::SetZoomLevelByExtension(
92 const scoped_refptr<const extensions::Extension>& extension) {
93 // Cannot zoom in disabled mode.
94 if (zoom_mode_ == ZOOM_MODE_DISABLED)
97 // Store extension data so that |extension| can be attributed when the zoom
98 // change completes. We expect that by the time this function returns that
99 // any observers that require this information will have requested it.
100 last_extension_ = extension;
102 // Do not actually rescale the page in manual mode.
103 if (zoom_mode_ == ZOOM_MODE_MANUAL) {
104 double old_zoom_level = zoom_level_;
105 zoom_level_ = zoom_level;
107 // TODO(wjmaclean) Do we care about filling in host/scheme here?
108 content::HostZoomMap::ZoomLevelChange change;
109 change.mode = content::HostZoomMap::ZOOM_CHANGED_TEMPORARY_ZOOM;
110 change.zoom_level = zoom_level;
111 ZoomEventManager::GetForBrowserContext(browser_context_)->
112 OnZoomLevelChanged(change);
114 ZoomChangedEventData zoom_change_data(web_contents(),
118 false /* can_show_bubble */);
120 ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
122 last_extension_ = NULL;
126 content::HostZoomMap* zoom_map =
127 content::HostZoomMap::GetForBrowserContext(browser_context_);
129 DCHECK(!event_data_);
130 event_data_.reset(new ZoomChangedEventData(web_contents(),
134 false /* can_show_bubble */));
135 int render_process_id = web_contents()->GetRenderProcessHost()->GetID();
136 int render_view_id = web_contents()->GetRenderViewHost()->GetRoutingID();
137 if (zoom_mode_ == ZOOM_MODE_ISOLATED ||
138 zoom_map->UsesTemporaryZoomLevel(render_process_id, render_view_id)) {
139 zoom_map->SetTemporaryZoomLevel(
140 render_process_id, render_view_id, zoom_level);
142 content::NavigationEntry* entry =
143 web_contents()->GetController().GetLastCommittedEntry();
146 last_extension_ = NULL;
149 std::string host = net::GetHostOrSpecFromURL(entry->GetURL());
150 zoom_map->SetZoomLevelForHost(host, zoom_level);
153 DCHECK(!event_data_);
154 last_extension_ = NULL;
158 void ZoomController::SetZoomMode(ZoomMode new_mode) {
159 if (new_mode == zoom_mode_)
162 content::HostZoomMap* zoom_map =
163 content::HostZoomMap::GetForBrowserContext(browser_context_);
165 int render_process_id = web_contents()->GetRenderProcessHost()->GetID();
166 int render_view_id = web_contents()->GetRenderViewHost()->GetRoutingID();
167 double original_zoom_level = GetZoomLevel();
169 DCHECK(!event_data_);
170 event_data_.reset(new ZoomChangedEventData(web_contents(),
174 new_mode != ZOOM_MODE_DEFAULT));
177 case ZOOM_MODE_DEFAULT: {
178 content::NavigationEntry* entry =
179 web_contents()->GetController().GetLastCommittedEntry();
182 GURL url = entry->GetURL();
183 std::string host = net::GetHostOrSpecFromURL(url);
185 if (zoom_map->HasZoomLevel(url.scheme(), host)) {
186 // If there are other tabs with the same origin, then set this tab's
187 // zoom level to match theirs. The temporary zoom level will be
188 // cleared below, but this call will make sure this tab re-draws at
189 // the correct zoom level.
190 double origin_zoom_level =
191 zoom_map->GetZoomLevelForHostAndScheme(url.scheme(), host);
192 event_data_->new_zoom_level = origin_zoom_level;
193 zoom_map->SetTemporaryZoomLevel(
194 render_process_id, render_view_id, origin_zoom_level);
196 // The host will need a level prior to removing the temporary level.
197 // We don't want the zoom level to change just because we entered
199 zoom_map->SetZoomLevelForHost(host, original_zoom_level);
202 // Remove per-tab zoom data for this tab. No event callback expected.
203 zoom_map->ClearTemporaryZoomLevel(render_process_id, render_view_id);
206 case ZOOM_MODE_ISOLATED: {
207 // Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
208 // page needs an initial isolated zoom back to the same level it was at
209 // in the other mode.
210 if (zoom_mode_ != ZOOM_MODE_DISABLED) {
211 zoom_map->SetTemporaryZoomLevel(
212 render_process_id, render_view_id, original_zoom_level);
214 // When we don't call any HostZoomMap set functions, we send the event
217 ZoomObserver, observers_, OnZoomChanged(*event_data_));
222 case ZOOM_MODE_MANUAL: {
223 // Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
224 // page needs to be resized to the default zoom. While in manual mode,
225 // the zoom level is handled independently.
226 if (zoom_mode_ != ZOOM_MODE_DISABLED) {
227 zoom_map->SetTemporaryZoomLevel(
228 render_process_id, render_view_id, default_zoom_level_.GetValue());
229 zoom_level_ = original_zoom_level;
231 // When we don't call any HostZoomMap set functions, we send the event
234 ZoomObserver, observers_, OnZoomChanged(*event_data_));
239 case ZOOM_MODE_DISABLED: {
240 // The page needs to be zoomed back to default before disabling the zoom
241 zoom_map->SetTemporaryZoomLevel(
242 render_process_id, render_view_id, default_zoom_level_.GetValue());
246 // Any event data we've stored should have been consumed by this point.
247 DCHECK(!event_data_);
249 zoom_mode_ = new_mode;
252 void ZoomController::DidNavigateMainFrame(
253 const content::LoadCommittedDetails& details,
254 const content::FrameNavigateParams& params) {
255 // If the main frame's content has changed, the new page may have a different
256 // zoom level from the old one.
257 UpdateState(std::string());
260 void ZoomController::WebContentsDestroyed() {
261 // At this point we should no longer be sending any zoom events with this
266 void ZoomController::OnZoomLevelChanged(
267 const content::HostZoomMap::ZoomLevelChange& change) {
268 UpdateState(change.host);
271 void ZoomController::UpdateState(const std::string& host) {
272 // If |host| is empty, all observers should be updated.
274 // Use the navigation entry's URL instead of the WebContents' so virtual
275 // URLs work (e.g. chrome://settings). http://crbug.com/153950
276 content::NavigationEntry* entry =
277 web_contents()->GetController().GetLastCommittedEntry();
279 host != net::GetHostOrSpecFromURL(entry->GetURL())) {
284 // The zoom bubble should not be shown for zoom changes where the host is
286 bool can_show_bubble = can_show_bubble_ && !host.empty();
289 // For state changes initiated within the ZoomController, information about
290 // the change should be sent.
291 ZoomChangedEventData zoom_change_data = *event_data_;
293 zoom_change_data.can_show_bubble = can_show_bubble;
295 ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
297 // TODO(wjmaclean) Should we consider having HostZoomMap send both old and
298 // new zoom levels here?
299 double zoom_level = GetZoomLevel();
300 ZoomChangedEventData zoom_change_data(
301 web_contents(), zoom_level, zoom_level, zoom_mode_, can_show_bubble);
303 ZoomObserver, observers_, OnZoomChanged(zoom_change_data));