opaque_browser_frame_view_layout.cc revision 58537e28ecd584eab876aee8be7156509866d23a
1// Copyright 2013 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/views/frame/opaque_browser_frame_view_layout.h"
6
7#include "ui/gfx/font.h"
8#include "ui/views/controls/button/image_button.h"
9#include "ui/views/controls/label.h"
10
11#if defined(OS_WIN)
12#include "win8/util/win8_util.h"
13#endif  // OS_WIN
14
15namespace {
16
17// Besides the frame border, there's another 9 px of empty space atop the
18// window in restored mode, to use to drag the window around.
19const int kNonClientRestoredExtraThickness = 9;
20
21// The titlebar never shrinks too short to show the caption button plus some
22// padding below it.
23const int kCaptionButtonHeightWithPadding = 19;
24
25// There is a 5 px gap between the title text and the caption buttons.
26const int kTitleLogoSpacing = 5;
27
28// The frame border is only visible in restored mode and is hardcoded to 4 px on
29// each side regardless of the system window border size.
30const int kFrameBorderThickness = 4;
31
32// The titlebar has a 2 px 3D edge along the top and bottom.
33const int kTitlebarTopAndBottomEdgeThickness = 2;
34
35// The icon is inset 2 px from the left frame border.
36const int kIconLeftSpacing = 2;
37
38// There is a 4 px gap between the icon and the title text.
39const int kIconTitleSpacing = 4;
40
41// The avatar ends 2 px above the bottom of the tabstrip (which, given the
42// way the tabstrip draws its bottom edge, will appear like a 1 px gap to the
43// user).
44const int kAvatarBottomSpacing = 2;
45
46// Space between the frame border and the left edge of the avatar.
47const int kAvatarLeftSpacing = 2;
48
49// Space between the right edge of the avatar and the tabstrip.
50const int kAvatarRightSpacing = -4;
51
52// In restored mode, the New Tab button isn't at the same height as the caption
53// buttons, but the space will look cluttered if it actually slides under them,
54// so we stop it when the gap between the two is down to 5 px.
55const int kNewTabCaptionRestoredSpacing = 5;
56
57// In maximized mode, where the New Tab button and the caption buttons are at
58// similar vertical coordinates, we need to reserve a larger, 16 px gap to avoid
59// looking too cluttered.
60const int kNewTabCaptionMaximizedSpacing = 16;
61
62// The top 3 px of the tabstrip is shadow; in maximized mode we push this off
63// the top of the screen so the tabs appear flush against the screen edge.
64const int kTabstripTopShadowThickness = 3;
65
66// How far to indent the tabstrip from the left side of the screen when there
67// is no avatar icon.
68const int kTabStripIndent = -6;
69
70#if defined(OS_LINUX) && !defined(OS_CHROMEOS)
71// Default extra space between the top of the frame and the top of the window
72// caption buttons.
73const int kExtraCaption = 2;
74
75// Default extra spacing between individual window caption buttons.
76const int kCaptionButtonSpacing = 2;
77#else
78const int kExtraCaption = 0;
79const int kCaptionButtonSpacing = 0;
80#endif
81
82}  // namespace
83
84///////////////////////////////////////////////////////////////////////////////
85// OpaqueBrowserFrameView, public:
86
87OpaqueBrowserFrameViewLayout::OpaqueBrowserFrameViewLayout(
88    OpaqueBrowserFrameViewLayoutDelegate* delegate)
89    : delegate_(delegate),
90      leading_button_start_(0),
91      trailing_button_start_(0),
92      minimum_size_for_buttons_(0),
93      has_leading_buttons_(false),
94      has_trailing_buttons_(false),
95      extra_caption_y_(kExtraCaption),
96      window_caption_spacing_(kCaptionButtonSpacing),
97      minimize_button_(NULL),
98      maximize_button_(NULL),
99      restore_button_(NULL),
100      close_button_(NULL),
101      window_icon_(NULL),
102      window_title_(NULL),
103      avatar_label_(NULL),
104      avatar_button_(NULL) {
105  trailing_buttons_.push_back(BUTTON_MINIMIZE);
106  trailing_buttons_.push_back(BUTTON_MAXIMIZE);
107  trailing_buttons_.push_back(BUTTON_CLOSE);
108}
109
110OpaqueBrowserFrameViewLayout::~OpaqueBrowserFrameViewLayout() {}
111
112// static
113bool OpaqueBrowserFrameViewLayout::ShouldAddDefaultCaptionButtons() {
114#if defined(OS_WIN)
115  return !win8::IsSingleWindowMetroMode();
116#endif  // OS_WIN
117  return true;
118}
119
120gfx::Rect OpaqueBrowserFrameViewLayout::GetBoundsForTabStrip(
121    const gfx::Size& tabstrip_preferred_size,
122    int available_width) const {
123  available_width -= trailing_button_start_;
124  available_width -= leading_button_start_;
125
126  if (delegate_->GetAdditionalReservedSpaceInTabStrip())
127    available_width -= delegate_->GetAdditionalReservedSpaceInTabStrip();
128
129  const int caption_spacing = delegate_->IsMaximized() ?
130      kNewTabCaptionMaximizedSpacing : kNewTabCaptionRestoredSpacing;
131  const int tabstrip_width = available_width - caption_spacing;
132  gfx::Rect bounds(leading_button_start_, GetTabStripInsetsTop(false),
133                   std::max(0, tabstrip_width),
134                   tabstrip_preferred_size.height());
135
136  int leading_tabstrip_indent = kTabStripIndent;
137  if (delegate_->ShouldShowAvatar()) {
138    if (avatar_label_ && avatar_label_->bounds().width()) {
139      // Space between the trailing edge of the avatar label and the tabstrip.
140      const int kAvatarLabelRightSpacing = -10;
141      leading_tabstrip_indent -= kAvatarLabelRightSpacing;
142    } else {
143      leading_tabstrip_indent -= kAvatarRightSpacing;
144    }
145  }
146  bounds.Inset(leading_tabstrip_indent, 0, 0, 0);
147  return bounds;
148}
149
150gfx::Size OpaqueBrowserFrameViewLayout::GetMinimumSize(
151    int available_width) const {
152  gfx::Size min_size = delegate_->GetBrowserViewMinimumSize();
153  int border_thickness = NonClientBorderThickness();
154  min_size.Enlarge(2 * border_thickness,
155                   NonClientTopBorderHeight(false) + border_thickness);
156
157  // Ensure that we can, at minimum, hold our window controls and avatar icon.
158  min_size.set_width(std::max(min_size.width(), minimum_size_for_buttons_));
159
160  // Ensure that the minimum width is enough to hold a minimum width tab strip
161  // at its usual insets.
162  if (delegate_->IsTabStripVisible()) {
163    gfx::Size preferred_size = delegate_->GetTabstripPreferredSize();
164    const int min_tabstrip_width = preferred_size.width();
165    const int caption_spacing = delegate_->IsMaximized() ?
166        kNewTabCaptionMaximizedSpacing : kNewTabCaptionRestoredSpacing;
167    min_size.Enlarge(min_tabstrip_width + caption_spacing, 0);
168  }
169
170  return min_size;
171}
172
173gfx::Rect OpaqueBrowserFrameViewLayout::GetWindowBoundsForClientBounds(
174    const gfx::Rect& client_bounds) const {
175  int top_height = NonClientTopBorderHeight(false);
176  int border_thickness = NonClientBorderThickness();
177  return gfx::Rect(std::max(0, client_bounds.x() - border_thickness),
178                   std::max(0, client_bounds.y() - top_height),
179                   client_bounds.width() + (2 * border_thickness),
180                   client_bounds.height() + top_height + border_thickness);
181}
182
183int OpaqueBrowserFrameViewLayout::FrameBorderThickness(bool restored) const {
184  return (!restored && (delegate_->IsMaximized() ||
185                        delegate_->IsFullscreen())) ?
186      0 : kFrameBorderThickness;
187}
188
189int OpaqueBrowserFrameViewLayout::NonClientBorderThickness() const {
190  // When we fill the screen, we don't show a client edge.
191  return FrameBorderThickness(false) +
192      ((delegate_->IsMaximized() || delegate_->IsFullscreen()) ?
193       0 : views::NonClientFrameView::kClientEdgeThickness);
194}
195
196int OpaqueBrowserFrameViewLayout::NonClientTopBorderHeight(
197    bool restored) const {
198  if (delegate_->ShouldShowWindowTitle()) {
199    return std::max(FrameBorderThickness(restored) + delegate_->GetIconSize(),
200        CaptionButtonY(restored) + kCaptionButtonHeightWithPadding) +
201        TitlebarBottomThickness(restored);
202  }
203
204  return FrameBorderThickness(restored) -
205      ((delegate_->IsTabStripVisible() &&
206          !restored && !delegate_->ShouldLeaveOffsetNearTopBorder())
207              ? kTabstripTopShadowThickness : 0);
208}
209
210int OpaqueBrowserFrameViewLayout::GetTabStripInsetsTop(bool restored) const {
211  return NonClientTopBorderHeight(restored) + ((!restored &&
212      (!delegate_->ShouldLeaveOffsetNearTopBorder() ||
213      delegate_->IsFullscreen())) ?
214      0 : kNonClientRestoredExtraThickness);
215}
216
217int OpaqueBrowserFrameViewLayout::TitlebarBottomThickness(bool restored) const {
218  return kTitlebarTopAndBottomEdgeThickness +
219      ((!restored && delegate_->IsMaximized()) ? 0 :
220       views::NonClientFrameView::kClientEdgeThickness);
221}
222
223int OpaqueBrowserFrameViewLayout::CaptionButtonY(bool restored) const {
224  // Maximized buttons start at window top so that even if their images aren't
225  // drawn flush with the screen edge, they still obey Fitts' Law.
226  return ((!restored && delegate_->IsMaximized()) ?
227      FrameBorderThickness(false) :
228          views::NonClientFrameView::kFrameShadowThickness) + extra_caption_y_;
229}
230
231gfx::Rect OpaqueBrowserFrameViewLayout::IconBounds() const {
232  return window_icon_bounds_;
233}
234
235gfx::Rect OpaqueBrowserFrameViewLayout::CalculateClientAreaBounds(
236    int width,
237    int height) const {
238  int top_height = NonClientTopBorderHeight(false);
239  int border_thickness = NonClientBorderThickness();
240  return gfx::Rect(border_thickness, top_height,
241                   std::max(0, width - (2 * border_thickness)),
242                   std::max(0, height - top_height - border_thickness));
243}
244
245///////////////////////////////////////////////////////////////////////////////
246// OpaqueBrowserFrameView, private:
247
248void OpaqueBrowserFrameViewLayout::LayoutWindowControls(views::View* host) {
249  if (!ShouldAddDefaultCaptionButtons())
250    return;
251
252  int caption_y = CaptionButtonY(false);
253
254  // Keep a list of all buttons that we don't show.
255  std::vector<ButtonID> buttons_not_shown;
256  buttons_not_shown.push_back(BUTTON_MAXIMIZE);
257  buttons_not_shown.push_back(BUTTON_MINIMIZE);
258  buttons_not_shown.push_back(BUTTON_CLOSE);
259
260  for (std::vector<ButtonID>::const_iterator it = leading_buttons_.begin();
261       it != leading_buttons_.end(); ++it) {
262    ConfigureButton(host, *it, ALIGN_LEADING, caption_y);
263    buttons_not_shown.erase(
264        std::remove(buttons_not_shown.begin(), buttons_not_shown.end(), *it),
265        buttons_not_shown.end());
266  }
267
268  for (std::vector<ButtonID>::const_reverse_iterator it =
269           trailing_buttons_.rbegin(); it != trailing_buttons_.rend(); ++it) {
270    ConfigureButton(host, *it, ALIGN_TRAILING, caption_y);
271    buttons_not_shown.erase(
272        std::remove(buttons_not_shown.begin(), buttons_not_shown.end(), *it),
273        buttons_not_shown.end());
274  }
275
276  for (std::vector<ButtonID>::const_iterator it = buttons_not_shown.begin();
277       it != buttons_not_shown.end(); ++it) {
278    HideButton(*it);
279  }
280}
281
282void OpaqueBrowserFrameViewLayout::LayoutTitleBar(views::View* host) {
283  bool use_hidden_icon_location = true;
284
285  int size = delegate_->GetIconSize();
286  int frame_thickness = FrameBorderThickness(false);
287  bool should_show_icon = delegate_->ShouldShowWindowIcon();
288  bool should_show_title = delegate_->ShouldShowWindowTitle();
289
290  if (should_show_icon || should_show_title) {
291    use_hidden_icon_location = false;
292
293    // Our frame border has a different "3D look" than Windows'.  Theirs has
294    // a more complex gradient on the top that they push their icon/title
295    // below; then the maximized window cuts this off and the icon/title are
296    // centered in the remaining space.  Because the apparent shape of our
297    // border is simpler, using the same positioning makes things look
298    // slightly uncentered with restored windows, so when the window is
299    // restored, instead of calculating the remaining space from below the
300    // frame border, we calculate from below the 3D edge.
301    int unavailable_px_at_top = delegate_->IsMaximized() ?
302        frame_thickness : kTitlebarTopAndBottomEdgeThickness;
303    // When the icon is shorter than the minimum space we reserve for the
304    // caption button, we vertically center it.  We want to bias rounding to
305    // put extra space above the icon, since the 3D edge (+ client edge, for
306    // restored windows) below looks (to the eye) more like additional space
307    // than does the 3D edge (or nothing at all, for maximized windows)
308    // above; hence the +1.
309    int y = unavailable_px_at_top + (NonClientTopBorderHeight(false) -
310                                     unavailable_px_at_top - size -
311                                     TitlebarBottomThickness(false) + 1) / 2;
312
313    window_icon_bounds_ = gfx::Rect(leading_button_start_ + kIconLeftSpacing, y,
314                                    size, size);
315    leading_button_start_ += size + kIconLeftSpacing;
316    minimum_size_for_buttons_ += size + kIconLeftSpacing;
317  }
318
319  if (should_show_icon)
320    window_icon_->SetBoundsRect(window_icon_bounds_);
321
322  if (window_title_) {
323    window_title_->SetVisible(should_show_title);
324    if (should_show_title) {
325      window_title_->SetText(delegate_->GetWindowTitle());
326
327      int text_width = std::max(
328          0, host->width() - trailing_button_start_ - kTitleLogoSpacing -
329          leading_button_start_ - kIconTitleSpacing);
330      window_title_->SetBounds(leading_button_start_ + kIconTitleSpacing,
331                               window_icon_bounds_.y(),
332                               text_width, window_icon_bounds_.height());
333      leading_button_start_ += text_width + kIconTitleSpacing;
334    }
335  }
336
337  if (use_hidden_icon_location) {
338    if (has_leading_buttons_) {
339      // There are window button icons on the left. Don't size the hidden window
340      // icon that people can double click on to close the window.
341      window_icon_bounds_ = gfx::Rect();
342    } else {
343      // We set the icon bounds to a small rectangle in the top leading corner
344      // if there are no icons on the leading side.
345      window_icon_bounds_ = gfx::Rect(
346          frame_thickness + kIconLeftSpacing, frame_thickness, size, size);
347    }
348  }
349}
350
351void OpaqueBrowserFrameViewLayout::LayoutAvatar() {
352  // Even though the avatar is used for both incognito and profiles we always
353  // use the incognito icon to layout the avatar button. The profile icon
354  // can be customized so we can't depend on its size to perform layout.
355  gfx::ImageSkia incognito_icon = delegate_->GetOTRAvatarIcon();
356
357  int avatar_bottom = GetTabStripInsetsTop(false) +
358      delegate_->GetTabStripHeight() - kAvatarBottomSpacing;
359  int avatar_restored_y = avatar_bottom - incognito_icon.height();
360  int avatar_y = delegate_->IsMaximized() ?
361      (NonClientTopBorderHeight(false) + kTabstripTopShadowThickness) :
362      avatar_restored_y;
363  avatar_bounds_.SetRect(leading_button_start_ + kAvatarLeftSpacing,
364      avatar_y, incognito_icon.width(),
365      delegate_->ShouldShowAvatar() ? (avatar_bottom - avatar_y) : 0);
366  if (avatar_button_) {
367    avatar_button_->SetBoundsRect(avatar_bounds_);
368    leading_button_start_ += kAvatarLeftSpacing + incognito_icon.width();
369    minimum_size_for_buttons_ += kAvatarLeftSpacing + incognito_icon.width();
370  }
371
372  if (avatar_label_) {
373    // Space between the bottom of the avatar and the bottom of the avatar
374    // label.
375    const int kAvatarLabelBottomSpacing = 3;
376    // Space between the frame border and the left edge of the avatar label.
377    const int kAvatarLabelLeftSpacing = -1;
378    gfx::Size label_size = avatar_label_->GetPreferredSize();
379    gfx::Rect label_bounds(
380        leading_button_start_ + kAvatarLabelLeftSpacing,
381        avatar_bottom - kAvatarLabelBottomSpacing - label_size.height(),
382        label_size.width(),
383        delegate_->ShouldShowAvatar() ? label_size.height() : 0);
384    avatar_label_->SetBoundsRect(label_bounds);
385    leading_button_start_ += kAvatarLabelLeftSpacing + label_size.width();
386  }
387}
388
389void OpaqueBrowserFrameViewLayout::ConfigureButton(
390    views::View* host,
391    ButtonID button_id,
392    ButtonAlignment alignment,
393    int caption_y) {
394  switch (button_id) {
395    case BUTTON_MINIMIZE: {
396      minimize_button_->SetVisible(true);
397      SetBoundsForButton(host, minimize_button_, alignment, caption_y);
398      break;
399    }
400    case BUTTON_MAXIMIZE: {
401      // When the window is restored, we show a maximized button; otherwise, we
402      // show a restore button.
403      bool is_restored = !delegate_->IsMaximized() && !delegate_->IsMinimized();
404      views::ImageButton* invisible_button = is_restored ?
405          restore_button_ : maximize_button_;
406      invisible_button->SetVisible(false);
407
408      views::ImageButton* visible_button = is_restored ?
409          maximize_button_ : restore_button_;
410      visible_button->SetVisible(true);
411      SetBoundsForButton(host, visible_button, alignment, caption_y);
412      break;
413    }
414    case BUTTON_CLOSE: {
415      close_button_->SetVisible(true);
416      SetBoundsForButton(host, close_button_, alignment, caption_y);
417      break;
418    }
419  }
420}
421
422void OpaqueBrowserFrameViewLayout::HideButton(ButtonID button_id) {
423  switch (button_id) {
424    case BUTTON_MINIMIZE:
425      minimize_button_->SetVisible(false);
426      break;
427    case BUTTON_MAXIMIZE:
428      restore_button_->SetVisible(false);
429      maximize_button_->SetVisible(false);
430      break;
431    case BUTTON_CLOSE:
432      close_button_->SetVisible(false);
433      break;
434  }
435}
436
437void OpaqueBrowserFrameViewLayout::SetBoundsForButton(
438    views::View* host,
439    views::ImageButton* button,
440    ButtonAlignment alignment,
441    int caption_y) {
442  gfx::Size button_size = button->GetPreferredSize();
443
444  button->SetImageAlignment(
445      (alignment == ALIGN_LEADING)  ?
446          views::ImageButton::ALIGN_RIGHT : views::ImageButton::ALIGN_LEFT,
447      views::ImageButton::ALIGN_BOTTOM);
448
449  // There should always be the same number of non-shadow pixels visible to the
450  // side of the caption buttons.  In maximized mode we extend the rightmost
451  // button to the screen corner to obey Fitts' Law.
452  bool is_maximized = delegate_->IsMaximized();
453
454  switch (alignment) {
455    case ALIGN_LEADING: {
456      if (has_leading_buttons_)
457        leading_button_start_ += window_caption_spacing_;
458
459      // If we're the first button on the left and maximized, add with to the
460      // right hand side of the screen.
461      int extra_width = (is_maximized && !has_leading_buttons_) ?
462        (kFrameBorderThickness -
463         views::NonClientFrameView::kFrameShadowThickness) : 0;
464
465      button->SetBounds(
466          leading_button_start_ - extra_width,
467          caption_y,
468          button_size.width() + extra_width,
469          button_size.height());
470
471      leading_button_start_ += extra_width + button_size.width();
472      minimum_size_for_buttons_ += extra_width + button_size.width();
473      has_leading_buttons_ = true;
474      break;
475    }
476    case ALIGN_TRAILING: {
477      if (has_trailing_buttons_)
478        trailing_button_start_ += window_caption_spacing_;
479
480      // If we're the first button on the right and maximized, add with to the
481      // right hand side of the screen.
482      int extra_width = (is_maximized && !has_trailing_buttons_) ?
483        (kFrameBorderThickness -
484         views::NonClientFrameView::kFrameShadowThickness) : 0;
485
486      button->SetBounds(
487          host->width() - trailing_button_start_ - extra_width -
488              button_size.width(),
489          caption_y,
490          button_size.width() + extra_width,
491          button_size.height());
492
493      trailing_button_start_ += extra_width + button_size.width();
494      minimum_size_for_buttons_ += extra_width + button_size.width();
495      has_trailing_buttons_ = true;
496      break;
497    }
498  }
499}
500
501void OpaqueBrowserFrameViewLayout::SetView(int id, views::View* view) {
502  // Why do things this way instead of having an Init() method, where we're
503  // passed the views we'll handle? Because OpaqueBrowserFrameView doesn't own
504  // all the views which are part of it. The avatar stuff, for example, will be
505  // added and removed by the base class of OpaqueBrowserFrameView.
506  switch (id) {
507    case VIEW_ID_MINIMIZE_BUTTON:
508      if (view) {
509        DCHECK_EQ(std::string(views::ImageButton::kViewClassName),
510                  view->GetClassName());
511      }
512      minimize_button_ = static_cast<views::ImageButton*>(view);
513      break;
514    case VIEW_ID_MAXIMIZE_BUTTON:
515      if (view) {
516        DCHECK_EQ(std::string(views::ImageButton::kViewClassName),
517                  view->GetClassName());
518      }
519      maximize_button_ = static_cast<views::ImageButton*>(view);
520      break;
521    case VIEW_ID_RESTORE_BUTTON:
522      if (view) {
523        DCHECK_EQ(std::string(views::ImageButton::kViewClassName),
524                  view->GetClassName());
525      }
526      restore_button_ = static_cast<views::ImageButton*>(view);
527      break;
528    case VIEW_ID_CLOSE_BUTTON:
529      if (view) {
530        DCHECK_EQ(std::string(views::ImageButton::kViewClassName),
531                  view->GetClassName());
532      }
533      close_button_ = static_cast<views::ImageButton*>(view);
534      break;
535    case VIEW_ID_WINDOW_ICON:
536      window_icon_ = view;
537      break;
538    case VIEW_ID_WINDOW_TITLE:
539      if (view) {
540        DCHECK_EQ(std::string(views::Label::kViewClassName),
541                  view->GetClassName());
542      }
543      window_title_ = static_cast<views::Label*>(view);
544      break;
545    case VIEW_ID_AVATAR_LABEL:
546      avatar_label_ = view;
547      break;
548    case VIEW_ID_AVATAR_BUTTON:
549      avatar_button_ = view;
550      break;
551    default:
552      NOTIMPLEMENTED() << "Unknown view id " << id;
553      break;
554  }
555}
556
557///////////////////////////////////////////////////////////////////////////////
558// OpaqueBrowserFrameView, views::LayoutManager:
559
560void OpaqueBrowserFrameViewLayout::Layout(views::View* host) {
561  // Reset all our data so that everything is invisible.
562  int thickness = FrameBorderThickness(false);
563  leading_button_start_ = thickness;
564  trailing_button_start_ = thickness;
565  minimum_size_for_buttons_ = leading_button_start_ + trailing_button_start_;
566  has_leading_buttons_ = false;
567  has_trailing_buttons_ = false;
568
569  LayoutWindowControls(host);
570  LayoutTitleBar(host);
571
572  // We now add a single pixel to the leading spacing. We do this because the
573  // avatar and tab strip start one pixel inward compared to where things start
574  // on the trailing side.
575  leading_button_start_++;
576
577  LayoutAvatar();
578
579  client_view_bounds_ = CalculateClientAreaBounds(
580      host->width(), host->height());
581}
582
583gfx::Size OpaqueBrowserFrameViewLayout::GetPreferredSize(views::View* host) {
584  // This is never used; NonClientView::GetPreferredSize() will be called
585  // instead.
586  NOTREACHED();
587  return gfx::Size();
588}
589
590void OpaqueBrowserFrameViewLayout::ViewAdded(views::View* host,
591                                             views::View* view) {
592  SetView(view->id(), view);
593}
594
595void OpaqueBrowserFrameViewLayout::ViewRemoved(views::View* host,
596                                               views::View* view) {
597  SetView(view->id(), NULL);
598}
599