summaryrefslogtreecommitdiffstats
path: root/chromium/ash/wm/caption_buttons/maximize_bubble_controller_bubble.cc
blob: d109a419265e957d4261e7e0734a5ae70886ce0d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
// Copyright 2013 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 "ash/wm/caption_buttons/maximize_bubble_controller_bubble.h"

#include "ash/metrics/user_metrics_recorder.h"
#include "ash/shell.h"
#include "ash/shell_window_ids.h"
#include "ash/wm/caption_buttons/bubble_contents_button_row.h"
#include "ash/wm/caption_buttons/frame_maximize_button.h"
#include "ash/wm/caption_buttons/maximize_bubble_controller.h"
#include "grit/ash_strings.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/animation/animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/path.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/mouse_watcher.h"


namespace ash {

// BubbleContentsView ---------------------------------------------------------

// A class which creates the content of the bubble: The buttons, and the label.
class BubbleContentsView : public views::View {
 public:
  BubbleContentsView(MaximizeBubbleControllerBubble* bubble,
                     SnapType initial_snap_type);
  virtual ~BubbleContentsView();

  // Set the label content to reflect the currently selected |snap_type|.
  // This function can be executed through the frame maximize button as well as
  // through hover operations.
  void SetSnapType(SnapType snap_type);

  // Added for unit test: Retrieve the button for an action.
  // |state| can be either SNAP_LEFT, SNAP_RIGHT or SNAP_MINIMIZE.
  views::CustomButton* GetButtonForUnitTest(SnapType state);

 private:
  // The owning class.
  MaximizeBubbleControllerBubble* bubble_;

  // The object which owns all the buttons.
  BubbleContentsButtonRow* buttons_view_;

  // The label object which shows the user the selected action.
  views::Label* label_view_;

  DISALLOW_COPY_AND_ASSIGN(BubbleContentsView);
};

BubbleContentsView::BubbleContentsView(
    MaximizeBubbleControllerBubble* bubble,
    SnapType initial_snap_type)
    : bubble_(bubble),
      buttons_view_(NULL),
      label_view_(NULL) {
  SetLayoutManager(new views::BoxLayout(
      views::BoxLayout::kVertical, 0, 0,
      MaximizeBubbleControllerBubble::kLayoutSpacing));
  set_background(views::Background::CreateSolidBackground(
      MaximizeBubbleControllerBubble::kBubbleBackgroundColor));

  buttons_view_ = new BubbleContentsButtonRow(bubble);
  AddChildView(buttons_view_);

  label_view_ = new views::Label();
  SetSnapType(initial_snap_type);
  label_view_->SetBackgroundColor(
      MaximizeBubbleControllerBubble::kBubbleBackgroundColor);
  const SkColor kBubbleTextColor = SK_ColorWHITE;
  label_view_->SetEnabledColor(kBubbleTextColor);
  const int kLabelSpacing = 4;
  label_view_->set_border(views::Border::CreateEmptyBorder(
      kLabelSpacing, 0, kLabelSpacing, 0));
  AddChildView(label_view_);
}

BubbleContentsView::~BubbleContentsView() {
}

// Set the label content to reflect the currently selected |snap_type|.
// This function can be executed through the frame maximize button as well as
// through hover operations.
void BubbleContentsView::SetSnapType(SnapType snap_type) {
  if (!bubble_->controller())
    return;

  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  int id = 0;
  switch (snap_type) {
    case SNAP_LEFT:
      id = IDS_ASH_SNAP_WINDOW_LEFT;
      break;
    case SNAP_RIGHT:
      id = IDS_ASH_SNAP_WINDOW_RIGHT;
      break;
    case SNAP_MAXIMIZE:
      DCHECK_NE(FRAME_STATE_FULL, bubble_->controller()->maximize_type());
      id = IDS_ASH_MAXIMIZE_WINDOW;
      break;
    case SNAP_MINIMIZE:
      id = IDS_ASH_MINIMIZE_WINDOW;
      break;
    case SNAP_RESTORE:
      DCHECK_NE(FRAME_STATE_NONE, bubble_->controller()->maximize_type());
      id = IDS_ASH_RESTORE_WINDOW;
      break;
    default:
      // If nothing is selected, we automatically select the click operation.
      id = bubble_->controller()->maximize_type() == FRAME_STATE_FULL ?
               IDS_ASH_RESTORE_WINDOW : IDS_ASH_MAXIMIZE_WINDOW;
      break;
  }
  label_view_->SetText(rb.GetLocalizedString(id));
}

views::CustomButton* BubbleContentsView::GetButtonForUnitTest(SnapType state) {
  return buttons_view_->GetButtonForUnitTest(state);
}


// MaximizeBubbleBorder -------------------------------------------------------

namespace {

const int kLineWidth = 1;
const int kArrowHeight = 10;
const int kArrowWidth = 20;

}  // namespace

class MaximizeBubbleBorder : public views::BubbleBorder {
 public:
  MaximizeBubbleBorder(views::View* content_view, views::View* anchor);

  virtual ~MaximizeBubbleBorder() {}

  // Get the mouse active area of the window.
  void GetMask(gfx::Path* mask);

  // views::BubbleBorder:
  virtual gfx::Rect GetBounds(const gfx::Rect& position_relative_to,
                              const gfx::Size& contents_size) const OVERRIDE;
  virtual void Paint(const views::View& view, gfx::Canvas* canvas) OVERRIDE;
  virtual gfx::Size GetMinimumSize() const OVERRIDE;

 private:
  // Note: Animations can continue after then main window frame was destroyed.
  // To avoid this problem, the owning screen metrics get extracted upon
  // creation.
  gfx::Size anchor_size_;
  gfx::Point anchor_screen_origin_;
  views::View* content_view_;

  DISALLOW_COPY_AND_ASSIGN(MaximizeBubbleBorder);
};

MaximizeBubbleBorder::MaximizeBubbleBorder(views::View* content_view,
                                           views::View* anchor)
    : views::BubbleBorder(
          views::BubbleBorder::TOP_RIGHT, views::BubbleBorder::NO_SHADOW,
          MaximizeBubbleControllerBubble::kBubbleBackgroundColor),
      anchor_size_(anchor->size()),
      anchor_screen_origin_(0, 0),
      content_view_(content_view) {
  views::View::ConvertPointToScreen(anchor, &anchor_screen_origin_);
  set_alignment(views::BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE);
}

void MaximizeBubbleBorder::GetMask(gfx::Path* mask) {
  gfx::Insets inset = GetInsets();
  // Note: Even though the tip could be added as activatable, it is left out
  // since it would not change the action behavior in any way plus it makes
  // more sense to keep the focus on the underlying button for clicks.
  int left = inset.left() - kLineWidth;
  int right = inset.left() + content_view_->width() + kLineWidth;
  int top = inset.top() - kLineWidth;
  int bottom = inset.top() + content_view_->height() + kLineWidth;
  mask->moveTo(left, top);
  mask->lineTo(right, top);
  mask->lineTo(right, bottom);
  mask->lineTo(left, bottom);
  mask->lineTo(left, top);
  mask->close();
}

gfx::Rect MaximizeBubbleBorder::GetBounds(
    const gfx::Rect& position_relative_to,
    const gfx::Size& contents_size) const {
  gfx::Size border_size(contents_size);
  gfx::Insets insets = GetInsets();
  border_size.Enlarge(insets.width(), insets.height());

  // Position the bubble to center the box on the anchor.
  int x = (anchor_size_.width() - border_size.width()) / 2;
  // Position the bubble under the anchor, overlapping the arrow with it.
  int y = anchor_size_.height() - insets.top();

  gfx::Point view_origin(x + anchor_screen_origin_.x(),
                         y + anchor_screen_origin_.y());

  return gfx::Rect(view_origin, border_size);
}

void MaximizeBubbleBorder::Paint(const views::View& view, gfx::Canvas* canvas) {
  gfx::Insets inset = GetInsets();

  // Draw the border line around everything.
  int y = inset.top();
  // Top
  canvas->FillRect(gfx::Rect(inset.left(),
                             y - kLineWidth,
                             content_view_->width(),
                             kLineWidth),
                   MaximizeBubbleControllerBubble::kBubbleBackgroundColor);
  // Bottom
  canvas->FillRect(gfx::Rect(inset.left(),
                             y + content_view_->height(),
                             content_view_->width(),
                             kLineWidth),
                   MaximizeBubbleControllerBubble::kBubbleBackgroundColor);
  // Left
  canvas->FillRect(gfx::Rect(inset.left() - kLineWidth,
                             y - kLineWidth,
                             kLineWidth,
                             content_view_->height() + 2 * kLineWidth),
                   MaximizeBubbleControllerBubble::kBubbleBackgroundColor);
  // Right
  canvas->FillRect(gfx::Rect(inset.left() + content_view_->width(),
                             y - kLineWidth,
                             kLineWidth,
                             content_view_->height() + 2 * kLineWidth),
                   MaximizeBubbleControllerBubble::kBubbleBackgroundColor);

  // Draw the arrow afterwards covering the border.
  SkPath path;
  path.incReserve(4);
  // The center of the tip should be in the middle of the button.
  int tip_x = inset.left() + content_view_->width() / 2;
  int left_base_x = tip_x - kArrowWidth / 2;
  int left_base_y = y;
  int tip_y = left_base_y - kArrowHeight;
  path.moveTo(SkIntToScalar(left_base_x), SkIntToScalar(left_base_y));
  path.lineTo(SkIntToScalar(tip_x), SkIntToScalar(tip_y));
  path.lineTo(SkIntToScalar(left_base_x + kArrowWidth),
              SkIntToScalar(left_base_y));

  SkPaint paint;
  paint.setStyle(SkPaint::kFill_Style);
  paint.setColor(MaximizeBubbleControllerBubble::kBubbleBackgroundColor);
  canvas->DrawPath(path, paint);
}

gfx::Size MaximizeBubbleBorder::GetMinimumSize() const {
  return gfx::Size(kLineWidth * 2 + kArrowWidth,
                   std::max(kLineWidth, kArrowHeight) + kLineWidth);
}


// BubbleMouseWatcherHost -----------------------------------------------------

// The mouse watcher host which makes sure that the bubble does not get closed
// while the mouse cursor is over the maximize button or the balloon content.
// Note: This object gets destroyed when the MouseWatcher gets destroyed.
class BubbleMouseWatcherHost: public views::MouseWatcherHost {
 public:
  explicit BubbleMouseWatcherHost(MaximizeBubbleControllerBubble* bubble);
  virtual ~BubbleMouseWatcherHost();

  // views::MouseWatcherHost:
  virtual bool Contains(const gfx::Point& screen_point,
                        views::MouseWatcherHost::MouseEventType type) OVERRIDE;
 private:
  MaximizeBubbleControllerBubble* bubble_;

  DISALLOW_COPY_AND_ASSIGN(BubbleMouseWatcherHost);
};

BubbleMouseWatcherHost::BubbleMouseWatcherHost(
    MaximizeBubbleControllerBubble* bubble)
    : bubble_(bubble) {
}

BubbleMouseWatcherHost::~BubbleMouseWatcherHost() {
}

bool BubbleMouseWatcherHost::Contains(
    const gfx::Point& screen_point,
    views::MouseWatcherHost::MouseEventType type) {
  return bubble_->Contains(screen_point, type);
}


// MaximizeBubbleControllerBubble ---------------------------------------------

// static
const SkColor MaximizeBubbleControllerBubble::kBubbleBackgroundColor =
    0xFF141414;
const int MaximizeBubbleControllerBubble::kLayoutSpacing = -1;

MaximizeBubbleControllerBubble::MaximizeBubbleControllerBubble(
    MaximizeBubbleController* owner,
    int appearance_delay_ms,
    SnapType initial_snap_type)
    : views::BubbleDelegateView(owner->frame_maximize_button(),
                                views::BubbleBorder::TOP_RIGHT),
      shutting_down_(false),
      owner_(owner),
      bubble_widget_(NULL),
      contents_view_(NULL),
      bubble_border_(NULL),
      appearance_delay_ms_(appearance_delay_ms) {
  set_margins(gfx::Insets());

  // The window needs to be owned by the root so that the SnapSizer does not
  // cover it upon animation.
  aura::Window* parent = Shell::GetContainer(
      Shell::GetTargetRootWindow(),
      internal::kShellWindowId_ShelfContainer);
  set_parent_window(parent);

  set_notify_enter_exit_on_child(true);
  set_adjust_if_offscreen(false);
  SetPaintToLayer(true);
  set_color(kBubbleBackgroundColor);
  set_close_on_deactivate(false);
  set_background(
      views::Background::CreateSolidBackground(kBubbleBackgroundColor));

  SetLayoutManager(new views::BoxLayout(
      views::BoxLayout::kVertical, 0, 0, kLayoutSpacing));

  contents_view_ = new BubbleContentsView(this, initial_snap_type);
  AddChildView(contents_view_);

  // Note that the returned widget has an observer which points to our
  // functions.
  bubble_widget_ = views::BubbleDelegateView::CreateBubble(this);
  bubble_widget_->set_focus_on_creation(false);

  SetAlignment(views::BubbleBorder::ALIGN_EDGE_TO_ANCHOR_EDGE);
  bubble_widget_->non_client_view()->frame_view()->set_background(NULL);

  bubble_border_ = new MaximizeBubbleBorder(this, GetAnchorView());
  GetBubbleFrameView()->SetBubbleBorder(bubble_border_);
  GetBubbleFrameView()->set_background(NULL);

  // Recalculate size with new border.
  SizeToContents();

  if (!appearance_delay_ms_)
    GetWidget()->Show();
  else
    StartFade(true);

  ash::Shell::GetInstance()->metrics()->RecordUserMetricsAction(
      ash::UMA_WINDOW_MAXIMIZE_BUTTON_SHOW_BUBBLE);

  mouse_watcher_.reset(new views::MouseWatcher(
      new BubbleMouseWatcherHost(this),
      this));
  mouse_watcher_->Start();
}

MaximizeBubbleControllerBubble::~MaximizeBubbleControllerBubble() {
}

aura::Window* MaximizeBubbleControllerBubble::GetBubbleWindow() {
  return bubble_widget_ ? bubble_widget_->GetNativeWindow() : NULL;
}

gfx::Rect MaximizeBubbleControllerBubble::GetAnchorRect() {
  if (!owner_)
    return gfx::Rect();

  gfx::Rect anchor_rect =
      owner_->frame_maximize_button()->GetBoundsInScreen();
  return anchor_rect;
}

void MaximizeBubbleControllerBubble::AnimationProgressed(
    const gfx::Animation* animation) {
  // First do everything needed for the fade by calling the base function.
  BubbleDelegateView::AnimationProgressed(animation);
  // When fading in we are done.
  if (!shutting_down_)
    return;
  // Upon fade out an additional shift is required.
  const int kBubbleAnimationOffsetY = 5;
  int shift = animation->CurrentValueBetween(kBubbleAnimationOffsetY, 0);
  gfx::Rect rect = initial_position_;

  rect.set_y(rect.y() + shift);
  bubble_widget_->GetNativeWindow()->SetBounds(rect);
}

bool MaximizeBubbleControllerBubble::CanActivate() const {
  return false;
}

bool MaximizeBubbleControllerBubble::WidgetHasHitTestMask() const {
  return bubble_border_ != NULL;
}

void MaximizeBubbleControllerBubble::GetWidgetHitTestMask(
    gfx::Path* mask) const {
  DCHECK(mask);
  DCHECK(bubble_border_);
  bubble_border_->GetMask(mask);
}

void MaximizeBubbleControllerBubble::MouseMovedOutOfHost() {
  if (!owner_ || shutting_down_)
    return;
  // When we leave the bubble, we might be still be in gesture mode or over
  // the maximize button. So only close if none of the other cases apply.
  if (!owner_->frame_maximize_button()->is_snap_enabled()) {
    gfx::Point screen_location = Shell::GetScreen()->GetCursorScreenPoint();
    if (!owner_->frame_maximize_button()->GetBoundsInScreen().Contains(
        screen_location)) {
      owner_->RequestDestructionThroughOwner();
    }
  }
}

bool MaximizeBubbleControllerBubble::Contains(
    const gfx::Point& screen_point,
    views::MouseWatcherHost::MouseEventType type) {
  if (!owner_ || shutting_down_)
    return false;
  bool inside_button =
      owner_->frame_maximize_button()->GetBoundsInScreen().Contains(
          screen_point);
  if (!owner_->frame_maximize_button()->is_snap_enabled() && inside_button) {
    SetSnapType(controller()->maximize_type() == FRAME_STATE_FULL ?
        SNAP_RESTORE : SNAP_MAXIMIZE);
    return true;
  }
  // Check if either a gesture is taking place (=> bubble stays no matter what
  // the mouse does) or the mouse is over the maximize button or the bubble
  // content.
  return (owner_->frame_maximize_button()->is_snap_enabled() ||
          inside_button ||
          contents_view_->GetBoundsInScreen().Contains(screen_point));
}

gfx::Size MaximizeBubbleControllerBubble::GetPreferredSize() {
  return contents_view_->GetPreferredSize();
}

void MaximizeBubbleControllerBubble::OnWidgetDestroying(views::Widget* widget) {
  if (bubble_widget_ == widget) {
    mouse_watcher_->Stop();

    if (owner_) {
      // If the bubble destruction was triggered by some other external
      // influence then ourselves, the owner needs to be informed that the menu
      // is gone.
      shutting_down_ = true;
      owner_->RequestDestructionThroughOwner();
      owner_ = NULL;
    }
  }
  BubbleDelegateView::OnWidgetDestroying(widget);
}

void MaximizeBubbleControllerBubble::ControllerRequestsCloseAndDelete() {
  // This only gets called from the owning base class once it is deleted.
  if (shutting_down_)
    return;
  shutting_down_ = true;
  owner_ = NULL;

  // Close the widget asynchronously after the hide animation is finished.
  initial_position_ = bubble_widget_->GetNativeWindow()->bounds();
  if (!appearance_delay_ms_)
    bubble_widget_->CloseNow();
  else
    StartFade(false);
}

void MaximizeBubbleControllerBubble::SetSnapType(SnapType snap_type) {
  if (contents_view_)
    contents_view_->SetSnapType(snap_type);
}

views::CustomButton* MaximizeBubbleControllerBubble::GetButtonForUnitTest(
    SnapType state) {
  return contents_view_->GetButtonForUnitTest(state);
}

}  // namespace ash