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.
4
5#include "chrome/browser/ui/zoom/zoom_controller.h"
6
7#include "chrome/browser/ui/sad_tab.h"
8#include "chrome/browser/ui/zoom/zoom_event_manager.h"
9#include "chrome/browser/ui/zoom/zoom_observer.h"
10#include "content/public/browser/host_zoom_map.h"
11#include "content/public/browser/navigation_entry.h"
12#include "content/public/browser/render_process_host.h"
13#include "content/public/browser/render_view_host.h"
14#include "content/public/browser/web_contents.h"
15#include "content/public/common/page_type.h"
16#include "content/public/common/page_zoom.h"
17#include "extensions/common/extension.h"
18#include "grit/theme_resources.h"
19#include "net/base/net_util.h"
20
21DEFINE_WEB_CONTENTS_USER_DATA_KEY(ZoomController);
22
23ZoomController::ZoomController(content::WebContents* web_contents)
24    : content::WebContentsObserver(web_contents),
25      can_show_bubble_(true),
26      zoom_mode_(ZOOM_MODE_DEFAULT),
27      zoom_level_(1.0),
28      browser_context_(web_contents->GetBrowserContext()) {
29  // TODO(wjmaclean) Make calls to HostZoomMap::GetDefaultForBrowserContext()
30  // refer to the webcontents-specific HostZoomMap when that becomes available.
31  content::HostZoomMap* host_zoom_map =
32      content::HostZoomMap::GetDefaultForBrowserContext(browser_context_);
33  zoom_level_ = host_zoom_map->GetDefaultZoomLevel();
34
35  zoom_subscription_ = host_zoom_map->AddZoomLevelChangedCallback(
36      base::Bind(&ZoomController::OnZoomLevelChanged, base::Unretained(this)));
37
38  UpdateState(std::string());
39}
40
41ZoomController::~ZoomController() {}
42
43bool ZoomController::IsAtDefaultZoom() const {
44  return content::ZoomValuesEqual(GetZoomLevel(), GetDefaultZoomLevel());
45}
46
47int ZoomController::GetResourceForZoomLevel() const {
48  if (IsAtDefaultZoom())
49    return IDR_ZOOM_NORMAL;
50  return GetZoomLevel() > GetDefaultZoomLevel() ? IDR_ZOOM_PLUS
51                                                : IDR_ZOOM_MINUS;
52}
53
54void ZoomController::AddObserver(ZoomObserver* observer) {
55  observers_.AddObserver(observer);
56}
57
58void ZoomController::RemoveObserver(ZoomObserver* observer) {
59  observers_.RemoveObserver(observer);
60}
61
62double ZoomController::GetZoomLevel() const {
63  return zoom_mode_ == ZOOM_MODE_MANUAL ?
64             zoom_level_:
65             content::HostZoomMap::GetZoomLevel(web_contents());
66}
67
68int ZoomController::GetZoomPercent() const {
69  double zoom_factor = content::ZoomLevelToZoomFactor(GetZoomLevel());
70  // Round double for return.
71  return static_cast<int>(zoom_factor * 100 + 0.5);
72}
73
74bool ZoomController::SetZoomLevel(double zoom_level) {
75  // An extension did not initiate this zoom change.
76  return SetZoomLevelByExtension(zoom_level, NULL);
77}
78
79bool ZoomController::SetZoomLevelByExtension(
80    double zoom_level,
81    const scoped_refptr<const extensions::Extension>& extension) {
82  content::NavigationEntry* entry =
83      web_contents()->GetController().GetLastCommittedEntry();
84  bool is_normal_page =
85      entry && entry->GetPageType() == content::PAGE_TYPE_NORMAL;
86  // Cannot zoom in disabled mode. Also, don't allow changing zoom level on
87  // a crashed tab, an error page or an interstitial page.
88  if (zoom_mode_ == ZOOM_MODE_DISABLED ||
89      !web_contents()->GetRenderViewHost()->IsRenderViewLive() ||
90      !is_normal_page)
91    return false;
92
93  // Store extension data so that |extension| can be attributed when the zoom
94  // change completes. We expect that by the time this function returns that
95  // any observers that require this information will have requested it.
96  last_extension_ = extension;
97
98  // Do not actually rescale the page in manual mode.
99  if (zoom_mode_ == ZOOM_MODE_MANUAL) {
100    double old_zoom_level = zoom_level_;
101    zoom_level_ = zoom_level;
102
103    // TODO(wjmaclean) Do we care about filling in host/scheme here?
104    content::HostZoomMap::ZoomLevelChange change;
105    change.mode = content::HostZoomMap::ZOOM_CHANGED_TEMPORARY_ZOOM;
106    change.zoom_level = zoom_level;
107    ZoomEventManager::GetForBrowserContext(browser_context_)->
108        OnZoomLevelChanged(change);
109
110    ZoomChangedEventData zoom_change_data(web_contents(),
111                                          old_zoom_level,
112                                          zoom_level_,
113                                          zoom_mode_,
114                                          false /* can_show_bubble */);
115    FOR_EACH_OBSERVER(
116        ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
117
118    last_extension_ = NULL;
119    return true;
120  }
121
122  content::HostZoomMap* zoom_map =
123      content::HostZoomMap::GetDefaultForBrowserContext(browser_context_);
124  DCHECK(zoom_map);
125  DCHECK(!event_data_);
126  event_data_.reset(new ZoomChangedEventData(web_contents(),
127                                             GetZoomLevel(),
128                                             zoom_level,
129                                             zoom_mode_,
130                                             false /* can_show_bubble */));
131  int render_process_id = web_contents()->GetRenderProcessHost()->GetID();
132  int render_view_id = web_contents()->GetRenderViewHost()->GetRoutingID();
133  if (zoom_mode_ == ZOOM_MODE_ISOLATED ||
134      zoom_map->UsesTemporaryZoomLevel(render_process_id, render_view_id)) {
135    zoom_map->SetTemporaryZoomLevel(
136        render_process_id, render_view_id, zoom_level);
137  } else {
138    if (!entry) {
139      last_extension_ = NULL;
140      return false;
141    }
142    std::string host = net::GetHostOrSpecFromURL(entry->GetURL());
143    zoom_map->SetZoomLevelForHost(host, zoom_level);
144  }
145
146  DCHECK(!event_data_);
147  last_extension_ = NULL;
148  return true;
149}
150
151void ZoomController::SetZoomMode(ZoomMode new_mode) {
152  if (new_mode == zoom_mode_)
153    return;
154
155  content::HostZoomMap* zoom_map =
156      content::HostZoomMap::GetDefaultForBrowserContext(browser_context_);
157  DCHECK(zoom_map);
158  int render_process_id = web_contents()->GetRenderProcessHost()->GetID();
159  int render_view_id = web_contents()->GetRenderViewHost()->GetRoutingID();
160  double original_zoom_level = GetZoomLevel();
161
162  DCHECK(!event_data_);
163  event_data_.reset(new ZoomChangedEventData(web_contents(),
164                                             original_zoom_level,
165                                             original_zoom_level,
166                                             new_mode,
167                                             new_mode != ZOOM_MODE_DEFAULT));
168
169  switch (new_mode) {
170    case ZOOM_MODE_DEFAULT: {
171      content::NavigationEntry* entry =
172          web_contents()->GetController().GetLastCommittedEntry();
173
174      if (entry) {
175        GURL url = entry->GetURL();
176        std::string host = net::GetHostOrSpecFromURL(url);
177
178        if (zoom_map->HasZoomLevel(url.scheme(), host)) {
179          // If there are other tabs with the same origin, then set this tab's
180          // zoom level to match theirs. The temporary zoom level will be
181          // cleared below, but this call will make sure this tab re-draws at
182          // the correct zoom level.
183          double origin_zoom_level =
184              zoom_map->GetZoomLevelForHostAndScheme(url.scheme(), host);
185          event_data_->new_zoom_level = origin_zoom_level;
186          zoom_map->SetTemporaryZoomLevel(
187              render_process_id, render_view_id, origin_zoom_level);
188        } else {
189          // The host will need a level prior to removing the temporary level.
190          // We don't want the zoom level to change just because we entered
191          // default mode.
192          zoom_map->SetZoomLevelForHost(host, original_zoom_level);
193        }
194      }
195      // Remove per-tab zoom data for this tab. No event callback expected.
196      zoom_map->ClearTemporaryZoomLevel(render_process_id, render_view_id);
197      break;
198    }
199    case ZOOM_MODE_ISOLATED: {
200      // Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
201      // page needs an initial isolated zoom back to the same level it was at
202      // in the other mode.
203      if (zoom_mode_ != ZOOM_MODE_DISABLED) {
204        zoom_map->SetTemporaryZoomLevel(
205            render_process_id, render_view_id, original_zoom_level);
206      } else {
207        // When we don't call any HostZoomMap set functions, we send the event
208        // manually.
209        FOR_EACH_OBSERVER(
210            ZoomObserver, observers_, OnZoomChanged(*event_data_));
211        event_data_.reset();
212      }
213      break;
214    }
215    case ZOOM_MODE_MANUAL: {
216      // Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
217      // page needs to be resized to the default zoom. While in manual mode,
218      // the zoom level is handled independently.
219      if (zoom_mode_ != ZOOM_MODE_DISABLED) {
220        zoom_map->SetTemporaryZoomLevel(
221            render_process_id, render_view_id, GetDefaultZoomLevel());
222        zoom_level_ = original_zoom_level;
223      } else {
224        // When we don't call any HostZoomMap set functions, we send the event
225        // manually.
226        FOR_EACH_OBSERVER(
227            ZoomObserver, observers_, OnZoomChanged(*event_data_));
228        event_data_.reset();
229      }
230      break;
231    }
232    case ZOOM_MODE_DISABLED: {
233      // The page needs to be zoomed back to default before disabling the zoom
234      zoom_map->SetTemporaryZoomLevel(
235          render_process_id, render_view_id, GetDefaultZoomLevel());
236      break;
237    }
238  }
239  // Any event data we've stored should have been consumed by this point.
240  DCHECK(!event_data_);
241
242  zoom_mode_ = new_mode;
243}
244
245void ZoomController::DidNavigateMainFrame(
246    const content::LoadCommittedDetails& details,
247    const content::FrameNavigateParams& params) {
248  // If the main frame's content has changed, the new page may have a different
249  // zoom level from the old one.
250  UpdateState(std::string());
251}
252
253void ZoomController::WebContentsDestroyed() {
254  // At this point we should no longer be sending any zoom events with this
255  // WebContents.
256  observers_.Clear();
257}
258
259void ZoomController::OnZoomLevelChanged(
260    const content::HostZoomMap::ZoomLevelChange& change) {
261  UpdateState(change.host);
262}
263
264void ZoomController::UpdateState(const std::string& host) {
265  // If |host| is empty, all observers should be updated.
266  if (!host.empty()) {
267    // Use the navigation entry's URL instead of the WebContents' so virtual
268    // URLs work (e.g. chrome://settings). http://crbug.com/153950
269    content::NavigationEntry* entry =
270        web_contents()->GetController().GetLastCommittedEntry();
271    if (!entry ||
272        host != net::GetHostOrSpecFromURL(entry->GetURL())) {
273      return;
274    }
275  }
276
277  // The zoom bubble should not be shown for zoom changes where the host is
278  // empty.
279  bool can_show_bubble = can_show_bubble_ && !host.empty();
280
281  if (event_data_) {
282    // For state changes initiated within the ZoomController, information about
283    // the change should be sent.
284    ZoomChangedEventData zoom_change_data = *event_data_;
285    event_data_.reset();
286    zoom_change_data.can_show_bubble = can_show_bubble;
287    FOR_EACH_OBSERVER(
288        ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
289  } else {
290    // TODO(wjmaclean) Should we consider having HostZoomMap send both old and
291    // new zoom levels here?
292    double zoom_level = GetZoomLevel();
293    ZoomChangedEventData zoom_change_data(
294        web_contents(), zoom_level, zoom_level, zoom_mode_, can_show_bubble);
295    FOR_EACH_OBSERVER(
296        ZoomObserver, observers_, OnZoomChanged(zoom_change_data));
297  }
298}
299