OLD | NEW |
| (Empty) |
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/notifications/balloon_collection_impl.h" | |
6 | |
7 #include "base/bind.h" | |
8 #include "base/logging.h" | |
9 #include "base/stl_util.h" | |
10 #include "chrome/browser/chrome_notification_types.h" | |
11 #include "chrome/browser/notifications/balloon.h" | |
12 #include "chrome/browser/notifications/balloon_host.h" | |
13 #include "chrome/browser/notifications/notification.h" | |
14 #include "chrome/browser/ui/browser.h" | |
15 #include "chrome/browser/ui/panels/docked_panel_collection.h" | |
16 #include "chrome/browser/ui/panels/panel.h" | |
17 #include "chrome/browser/ui/panels/panel_manager.h" | |
18 #include "content/public/browser/notification_registrar.h" | |
19 #include "content/public/browser/notification_service.h" | |
20 #include "ui/gfx/rect.h" | |
21 #include "ui/gfx/screen.h" | |
22 #include "ui/gfx/size.h" | |
23 | |
24 // Portion of the screen allotted for notifications. When notification balloons | |
25 // extend over this, no new notifications are shown until some are closed. | |
26 const double kPercentBalloonFillFactor = 0.7; | |
27 | |
28 // Allow at least this number of balloons on the screen. | |
29 const int kMinAllowedBalloonCount = 2; | |
30 | |
31 // The spacing between the balloon and the panel. | |
32 const int kVerticalSpacingBetweenBalloonAndPanel = 5; | |
33 | |
34 // Delay from the mouse leaving the balloon collection before | |
35 // there is a relayout, in milliseconds. | |
36 const int kRepositionDelayMs = 300; | |
37 | |
38 | |
39 BalloonCollectionImpl::BalloonCollectionImpl() | |
40 : reposition_factory_(this), | |
41 added_as_message_loop_observer_(false) { | |
42 registrar_.Add(this, chrome::NOTIFICATION_PANEL_COLLECTION_UPDATED, | |
43 content::NotificationService::AllSources()); | |
44 registrar_.Add(this, chrome::NOTIFICATION_PANEL_CHANGED_EXPANSION_STATE, | |
45 content::NotificationService::AllSources()); | |
46 | |
47 SetPositionPreference(BalloonCollection::DEFAULT_POSITION); | |
48 } | |
49 | |
50 BalloonCollectionImpl::~BalloonCollectionImpl() { | |
51 RemoveMessageLoopObserver(); | |
52 } | |
53 | |
54 void BalloonCollectionImpl::AddImpl(const Notification& notification, | |
55 Profile* profile, | |
56 bool add_to_front) { | |
57 Balloon* new_balloon = MakeBalloon(notification, profile); | |
58 // The +1 on width is necessary because width is fixed on notifications, | |
59 // so since we always have the max size, we would always hit the scrollbar | |
60 // condition. We are only interested in comparing height to maximum. | |
61 new_balloon->set_min_scrollbar_size(gfx::Size(1 + layout_.max_balloon_width(), | |
62 layout_.max_balloon_height())); | |
63 new_balloon->SetPosition(layout_.OffScreenLocation(), false); | |
64 new_balloon->Show(); | |
65 int count = base_.count(); | |
66 if (count > 0 && layout_.RequiresOffsets()) | |
67 new_balloon->set_offset(base_.balloons()[count - 1]->offset()); | |
68 base_.Add(new_balloon, add_to_front); | |
69 PositionBalloons(false); | |
70 | |
71 // There may be no listener in a unit test. | |
72 if (space_change_listener_) | |
73 space_change_listener_->OnBalloonSpaceChanged(); | |
74 | |
75 // This is used only for testing. | |
76 if (!on_collection_changed_callback_.is_null()) | |
77 on_collection_changed_callback_.Run(); | |
78 } | |
79 | |
80 void BalloonCollectionImpl::Add(const Notification& notification, | |
81 Profile* profile) { | |
82 AddImpl(notification, profile, false); | |
83 } | |
84 | |
85 const Notification* BalloonCollectionImpl::FindById( | |
86 const std::string& id) const { | |
87 return base_.FindById(id); | |
88 } | |
89 | |
90 bool BalloonCollectionImpl::RemoveById(const std::string& id) { | |
91 return base_.CloseById(id); | |
92 } | |
93 | |
94 bool BalloonCollectionImpl::RemoveBySourceOrigin(const GURL& origin) { | |
95 return base_.CloseAllBySourceOrigin(origin); | |
96 } | |
97 | |
98 bool BalloonCollectionImpl::RemoveByProfile(Profile* profile) { | |
99 return base_.CloseAllByProfile(profile); | |
100 } | |
101 | |
102 void BalloonCollectionImpl::RemoveAll() { | |
103 base_.CloseAll(); | |
104 } | |
105 | |
106 bool BalloonCollectionImpl::HasSpace() const { | |
107 int count = base_.count(); | |
108 if (count < kMinAllowedBalloonCount) | |
109 return true; | |
110 | |
111 int max_balloon_size = 0; | |
112 int total_size = 0; | |
113 layout_.GetMaxLinearSize(&max_balloon_size, &total_size); | |
114 | |
115 int current_max_size = max_balloon_size * count; | |
116 int max_allowed_size = static_cast<int>(total_size * | |
117 kPercentBalloonFillFactor); | |
118 return current_max_size < max_allowed_size - max_balloon_size; | |
119 } | |
120 | |
121 void BalloonCollectionImpl::ResizeBalloon(Balloon* balloon, | |
122 const gfx::Size& size) { | |
123 balloon->set_content_size(Layout::ConstrainToSizeLimits(size)); | |
124 PositionBalloons(true); | |
125 } | |
126 | |
127 void BalloonCollectionImpl::DisplayChanged() { | |
128 layout_.RefreshSystemMetrics(); | |
129 PositionBalloons(true); | |
130 } | |
131 | |
132 void BalloonCollectionImpl::OnBalloonClosed(Balloon* source) { | |
133 // We want to free the balloon when finished. | |
134 const Balloons& balloons = base_.balloons(); | |
135 | |
136 Balloons::const_iterator it = balloons.begin(); | |
137 if (layout_.RequiresOffsets()) { | |
138 gfx::Vector2d offset; | |
139 bool apply_offset = false; | |
140 while (it != balloons.end()) { | |
141 if (*it == source) { | |
142 ++it; | |
143 if (it != balloons.end()) { | |
144 apply_offset = true; | |
145 offset.set_y((source)->offset().y() - (*it)->offset().y() + | |
146 (*it)->content_size().height() - source->content_size().height()); | |
147 } | |
148 } else { | |
149 if (apply_offset) | |
150 (*it)->add_offset(offset); | |
151 ++it; | |
152 } | |
153 } | |
154 // Start listening for UI events so we cancel the offset when the mouse | |
155 // leaves the balloon area. | |
156 if (apply_offset) | |
157 AddMessageLoopObserver(); | |
158 } | |
159 | |
160 base_.Remove(source); | |
161 PositionBalloons(true); | |
162 | |
163 // There may be no listener in a unit test. | |
164 if (space_change_listener_) | |
165 space_change_listener_->OnBalloonSpaceChanged(); | |
166 | |
167 // This is used only for testing. | |
168 if (!on_collection_changed_callback_.is_null()) | |
169 on_collection_changed_callback_.Run(); | |
170 } | |
171 | |
172 const BalloonCollection::Balloons& BalloonCollectionImpl::GetActiveBalloons() { | |
173 return base_.balloons(); | |
174 } | |
175 | |
176 void BalloonCollectionImpl::Observe( | |
177 int type, | |
178 const content::NotificationSource& source, | |
179 const content::NotificationDetails& details) { | |
180 gfx::Rect bounds; | |
181 switch (type) { | |
182 case chrome::NOTIFICATION_PANEL_COLLECTION_UPDATED: | |
183 case chrome::NOTIFICATION_PANEL_CHANGED_EXPANSION_STATE: | |
184 layout_.enable_computing_panel_offset(); | |
185 if (layout_.ComputeOffsetToMoveAbovePanels()) | |
186 PositionBalloons(true); | |
187 break; | |
188 default: | |
189 NOTREACHED(); | |
190 break; | |
191 } | |
192 } | |
193 | |
194 void BalloonCollectionImpl::PositionBalloonsInternal(bool reposition) { | |
195 const Balloons& balloons = base_.balloons(); | |
196 | |
197 layout_.RefreshSystemMetrics(); | |
198 gfx::Point origin = layout_.GetLayoutOrigin(); | |
199 for (Balloons::const_iterator it = balloons.begin(); | |
200 it != balloons.end(); | |
201 ++it) { | |
202 gfx::Point upper_left = layout_.NextPosition((*it)->GetViewSize(), &origin); | |
203 (*it)->SetPosition(upper_left, reposition); | |
204 } | |
205 } | |
206 | |
207 gfx::Rect BalloonCollectionImpl::GetBalloonsBoundingBox() const { | |
208 // Start from the layout origin. | |
209 gfx::Rect bounds = gfx::Rect(layout_.GetLayoutOrigin(), gfx::Size(0, 0)); | |
210 | |
211 // For each balloon, extend the rectangle. This approach is indifferent to | |
212 // the orientation of the balloons. | |
213 const Balloons& balloons = base_.balloons(); | |
214 Balloons::const_iterator iter; | |
215 for (iter = balloons.begin(); iter != balloons.end(); ++iter) { | |
216 gfx::Rect balloon_box = gfx::Rect((*iter)->GetPosition(), | |
217 (*iter)->GetViewSize()); | |
218 bounds.Union(balloon_box); | |
219 } | |
220 | |
221 return bounds; | |
222 } | |
223 | |
224 void BalloonCollectionImpl::AddMessageLoopObserver() { | |
225 if (!added_as_message_loop_observer_) { | |
226 base::MessageLoopForUI::current()->AddObserver(this); | |
227 added_as_message_loop_observer_ = true; | |
228 } | |
229 } | |
230 | |
231 void BalloonCollectionImpl::RemoveMessageLoopObserver() { | |
232 if (added_as_message_loop_observer_) { | |
233 base::MessageLoopForUI::current()->RemoveObserver(this); | |
234 added_as_message_loop_observer_ = false; | |
235 } | |
236 } | |
237 | |
238 void BalloonCollectionImpl::CancelOffsets() { | |
239 reposition_factory_.InvalidateWeakPtrs(); | |
240 | |
241 // Unhook from listening to all UI events. | |
242 RemoveMessageLoopObserver(); | |
243 | |
244 const Balloons& balloons = base_.balloons(); | |
245 for (Balloons::const_iterator it = balloons.begin(); | |
246 it != balloons.end(); | |
247 ++it) | |
248 (*it)->set_offset(gfx::Vector2d()); | |
249 | |
250 PositionBalloons(true); | |
251 } | |
252 | |
253 void BalloonCollectionImpl::HandleMouseMoveEvent() { | |
254 if (!IsCursorInBalloonCollection()) { | |
255 // Mouse has left the region. Schedule a reposition after | |
256 // a short delay. | |
257 if (!reposition_factory_.HasWeakPtrs()) { | |
258 base::MessageLoop::current()->PostDelayedTask( | |
259 FROM_HERE, | |
260 base::Bind(&BalloonCollectionImpl::CancelOffsets, | |
261 reposition_factory_.GetWeakPtr()), | |
262 base::TimeDelta::FromMilliseconds(kRepositionDelayMs)); | |
263 } | |
264 } else { | |
265 // Mouse moved back into the region. Cancel the reposition. | |
266 reposition_factory_.InvalidateWeakPtrs(); | |
267 } | |
268 } | |
269 | |
270 BalloonCollectionImpl::Layout::Layout() | |
271 : placement_(INVALID), | |
272 need_to_compute_panel_offset_(false), | |
273 offset_to_move_above_panels_(0) { | |
274 RefreshSystemMetrics(); | |
275 } | |
276 | |
277 void BalloonCollectionImpl::Layout::GetMaxLinearSize(int* max_balloon_size, | |
278 int* total_size) const { | |
279 DCHECK(max_balloon_size && total_size); | |
280 | |
281 // All placement schemes are vertical, so we only care about height. | |
282 *total_size = work_area_.height(); | |
283 *max_balloon_size = max_balloon_height(); | |
284 } | |
285 | |
286 gfx::Point BalloonCollectionImpl::Layout::GetLayoutOrigin() const { | |
287 // For lower-left and lower-right positioning, we need to add an offset | |
288 // to ensure balloons to stay on top of panels to avoid overlapping. | |
289 int x = 0; | |
290 int y = 0; | |
291 switch (placement_) { | |
292 case VERTICALLY_FROM_TOP_LEFT: { | |
293 x = work_area_.x() + HorizontalEdgeMargin(); | |
294 y = work_area_.y() + VerticalEdgeMargin() + offset_to_move_above_panels_; | |
295 break; | |
296 } | |
297 case VERTICALLY_FROM_TOP_RIGHT: { | |
298 x = work_area_.right() - HorizontalEdgeMargin(); | |
299 y = work_area_.y() + VerticalEdgeMargin() + offset_to_move_above_panels_; | |
300 break; | |
301 } | |
302 case VERTICALLY_FROM_BOTTOM_LEFT: | |
303 x = work_area_.x() + HorizontalEdgeMargin(); | |
304 y = work_area_.bottom() - VerticalEdgeMargin() - | |
305 offset_to_move_above_panels_; | |
306 break; | |
307 case VERTICALLY_FROM_BOTTOM_RIGHT: | |
308 x = work_area_.right() - HorizontalEdgeMargin(); | |
309 y = work_area_.bottom() - VerticalEdgeMargin() - | |
310 offset_to_move_above_panels_; | |
311 break; | |
312 default: | |
313 NOTREACHED(); | |
314 break; | |
315 } | |
316 return gfx::Point(x, y); | |
317 } | |
318 | |
319 gfx::Point BalloonCollectionImpl::Layout::NextPosition( | |
320 const gfx::Size& balloon_size, | |
321 gfx::Point* position_iterator) const { | |
322 DCHECK(position_iterator); | |
323 | |
324 int x = 0; | |
325 int y = 0; | |
326 switch (placement_) { | |
327 case VERTICALLY_FROM_TOP_LEFT: | |
328 x = position_iterator->x(); | |
329 y = position_iterator->y(); | |
330 position_iterator->set_y(position_iterator->y() + balloon_size.height() + | |
331 InterBalloonMargin()); | |
332 break; | |
333 case VERTICALLY_FROM_TOP_RIGHT: | |
334 x = position_iterator->x() - balloon_size.width(); | |
335 y = position_iterator->y(); | |
336 position_iterator->set_y(position_iterator->y() + balloon_size.height() + | |
337 InterBalloonMargin()); | |
338 break; | |
339 case VERTICALLY_FROM_BOTTOM_LEFT: | |
340 position_iterator->set_y(position_iterator->y() - balloon_size.height() - | |
341 InterBalloonMargin()); | |
342 x = position_iterator->x(); | |
343 y = position_iterator->y(); | |
344 break; | |
345 case VERTICALLY_FROM_BOTTOM_RIGHT: | |
346 position_iterator->set_y(position_iterator->y() - balloon_size.height() - | |
347 InterBalloonMargin()); | |
348 x = position_iterator->x() - balloon_size.width(); | |
349 y = position_iterator->y(); | |
350 break; | |
351 default: | |
352 NOTREACHED(); | |
353 break; | |
354 } | |
355 return gfx::Point(x, y); | |
356 } | |
357 | |
358 gfx::Point BalloonCollectionImpl::Layout::OffScreenLocation() const { | |
359 gfx::Point location = GetLayoutOrigin(); | |
360 switch (placement_) { | |
361 case VERTICALLY_FROM_TOP_LEFT: | |
362 case VERTICALLY_FROM_BOTTOM_LEFT: | |
363 location.Offset(0, kBalloonMaxHeight); | |
364 break; | |
365 case VERTICALLY_FROM_TOP_RIGHT: | |
366 case VERTICALLY_FROM_BOTTOM_RIGHT: | |
367 location.Offset(-kBalloonMaxWidth - BalloonView::GetHorizontalMargin(), | |
368 kBalloonMaxHeight); | |
369 break; | |
370 default: | |
371 NOTREACHED(); | |
372 break; | |
373 } | |
374 return location; | |
375 } | |
376 | |
377 bool BalloonCollectionImpl::Layout::RequiresOffsets() const { | |
378 // Layout schemes that grow up from the bottom require offsets; | |
379 // schemes that grow down do not require offsets. | |
380 bool offsets = (placement_ == VERTICALLY_FROM_BOTTOM_LEFT || | |
381 placement_ == VERTICALLY_FROM_BOTTOM_RIGHT); | |
382 return offsets; | |
383 } | |
384 | |
385 // static | |
386 gfx::Size BalloonCollectionImpl::Layout::ConstrainToSizeLimits( | |
387 const gfx::Size& size) { | |
388 // restrict to the min & max sizes | |
389 return gfx::Size( | |
390 std::max(min_balloon_width(), | |
391 std::min(max_balloon_width(), size.width())), | |
392 std::max(min_balloon_height(), | |
393 std::min(max_balloon_height(), size.height()))); | |
394 } | |
395 | |
396 bool BalloonCollectionImpl::Layout::ComputeOffsetToMoveAbovePanels() { | |
397 // If the offset is not enabled due to that we have not received a | |
398 // notification about panel, don't proceed because we don't want to call | |
399 // PanelManager::GetInstance() to create an instance when panel is not | |
400 // present. | |
401 if (!need_to_compute_panel_offset_) | |
402 return false; | |
403 | |
404 const DockedPanelCollection::Panels& panels = | |
405 PanelManager::GetInstance()->docked_collection()->panels(); | |
406 int offset_to_move_above_panels = 0; | |
407 | |
408 // The offset is the maximum height of panels that could overlap with the | |
409 // balloons. | |
410 if (NeedToMoveAboveLeftSidePanels()) { | |
411 for (DockedPanelCollection::Panels::const_reverse_iterator iter = | |
412 panels.rbegin(); | |
413 iter != panels.rend(); ++iter) { | |
414 // No need to check panels beyond the area occupied by the balloons. | |
415 if ((*iter)->GetBounds().x() >= work_area_.x() + max_balloon_width()) | |
416 break; | |
417 | |
418 int current_height = (*iter)->GetBounds().height(); | |
419 if (current_height > offset_to_move_above_panels) | |
420 offset_to_move_above_panels = current_height; | |
421 } | |
422 } else if (NeedToMoveAboveRightSidePanels()) { | |
423 for (DockedPanelCollection::Panels::const_iterator iter = panels.begin(); | |
424 iter != panels.end(); ++iter) { | |
425 // No need to check panels beyond the area occupied by the balloons. | |
426 if ((*iter)->GetBounds().right() <= | |
427 work_area_.right() - max_balloon_width()) | |
428 break; | |
429 | |
430 int current_height = (*iter)->GetBounds().height(); | |
431 if (current_height > offset_to_move_above_panels) | |
432 offset_to_move_above_panels = current_height; | |
433 } | |
434 } | |
435 | |
436 // Ensure that we have some sort of margin between the 1st balloon and the | |
437 // panel beneath it even the vertical edge margin is 0 as on Mac. | |
438 if (offset_to_move_above_panels && !VerticalEdgeMargin()) | |
439 offset_to_move_above_panels += kVerticalSpacingBetweenBalloonAndPanel; | |
440 | |
441 // If no change is detected, return false to indicate that we do not need to | |
442 // reposition balloons. | |
443 if (offset_to_move_above_panels_ == offset_to_move_above_panels) | |
444 return false; | |
445 | |
446 offset_to_move_above_panels_ = offset_to_move_above_panels; | |
447 return true; | |
448 } | |
449 | |
450 bool BalloonCollectionImpl::Layout::RefreshSystemMetrics() { | |
451 bool changed = false; | |
452 | |
453 // TODO(scottmg): NativeScreen is wrong. http://crbug.com/133312 | |
454 gfx::Rect new_work_area = | |
455 gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area(); | |
456 if (work_area_ != new_work_area) { | |
457 work_area_.SetRect(new_work_area.x(), new_work_area.y(), | |
458 new_work_area.width(), new_work_area.height()); | |
459 changed = true; | |
460 } | |
461 | |
462 return changed; | |
463 } | |
OLD | NEW |