Commit 85cd2a96 authored by Robert Sesek's avatar Robert Sesek Committed by Commit Bot

Delete Cocoa message_center and switch to the Views version.

This also removes some now-unused Cocoa control classes in //ui/base and
several unneeded resources.

Bug: 739386, 832676
Change-Id: I9289cc5613c4303521aaed9271ddc7574e2611a0
Reviewed-on: https://chromium-review.googlesource.com/c/1343055Reviewed-by: default avatarNico Weber <thakis@chromium.org>
Reviewed-by: default avatarEvan Stade <estade@chromium.org>
Commit-Queue: Robert Sesek <rsesek@chromium.org>
Cr-Commit-Position: refs/heads/master@{#609810}
parent 0fc5f9d3
...@@ -1817,6 +1817,8 @@ jumbo_split_static_library("ui") { ...@@ -1817,6 +1817,8 @@ jumbo_split_static_library("ui") {
"user_manager.h", "user_manager.h",
"views/external_protocol_dialog.cc", "views/external_protocol_dialog.cc",
"views/external_protocol_dialog.h", "views/external_protocol_dialog.h",
"views/message_center/popups_only_ui_delegate.cc",
"views/message_center/popups_only_ui_delegate.h",
"views/profiles/badged_profile_photo.cc", "views/profiles/badged_profile_photo.cc",
"views/profiles/badged_profile_photo.h", "views/profiles/badged_profile_photo.h",
"views/profiles/dice_accounts_menu.cc", "views/profiles/dice_accounts_menu.cc",
...@@ -2027,8 +2029,6 @@ jumbo_split_static_library("ui") { ...@@ -2027,8 +2029,6 @@ jumbo_split_static_library("ui") {
"cocoa/media_picker/desktop_media_picker_item.mm", "cocoa/media_picker/desktop_media_picker_item.mm",
"cocoa/native_window_tracker_cocoa.h", "cocoa/native_window_tracker_cocoa.h",
"cocoa/native_window_tracker_cocoa.mm", "cocoa/native_window_tracker_cocoa.mm",
"cocoa/notifications/message_center_bridge.h",
"cocoa/notifications/message_center_bridge.mm",
"cocoa/nsmenuitem_additions.h", "cocoa/nsmenuitem_additions.h",
"cocoa/nsmenuitem_additions.mm", "cocoa/nsmenuitem_additions.mm",
"cocoa/profiles/profile_menu_controller.h", "cocoa/profiles/profile_menu_controller.h",
...@@ -3035,8 +3035,6 @@ jumbo_split_static_library("ui") { ...@@ -3035,8 +3035,6 @@ jumbo_split_static_library("ui") {
"views/frame/browser_non_client_frame_view_factory_views.cc", "views/frame/browser_non_client_frame_view_factory_views.cc",
"views/frame/desktop_browser_frame_aura.cc", "views/frame/desktop_browser_frame_aura.cc",
"views/frame/desktop_browser_frame_aura.h", "views/frame/desktop_browser_frame_aura.h",
"views/message_center/popups_only_ui_delegate.cc",
"views/message_center/popups_only_ui_delegate.h",
] ]
} }
} }
......
// Copyright (c) 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.
#ifndef CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_MESSAGE_CENTER_BRIDGE_H_
#define CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_MESSAGE_CENTER_BRIDGE_H_
#import <AppKit/AppKit.h>
#include <memory>
#include "base/mac/scoped_nsobject.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/notifications/popups_only_ui_controller.h"
#include "components/prefs/pref_member.h"
#include "ui/message_center/message_center.h"
@class MCPopupCollection;
namespace message_center {
class MessageCenter;
} // namespace message_center
// MessageCenterBridge is the owner of all the Cocoa UI objects for the
// message_center. It bridges C++ notifications from the PoupsOnlyUiController
// to the various UI objects.
class MessageCenterBridge : public PopupsOnlyUiController::Delegate {
public:
explicit MessageCenterBridge(message_center::MessageCenter* message_center);
~MessageCenterBridge() override;
// PopupsUiController::Delegate:
void ShowPopups() override;
void HidePopups() override;
private:
friend class MessageCenterBridgeTest;
// The global, singleton message center model object. Weak.
message_center::MessageCenter* const message_center_;
// Obj-C controller for the on-screen popup notifications.
base::scoped_nsobject<MCPopupCollection> popup_collection_;
DISALLOW_COPY_AND_ASSIGN(MessageCenterBridge);
};
#endif // CHROME_BROWSER_UI_COCOA_NOTIFICATIONS_MESSAGE_CENTER_BRIDGE_H_
// Copyright (c) 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 "chrome/browser/ui/cocoa/notifications/message_center_bridge.h"
#include "base/bind.h"
#include "base/i18n/number_formatting.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#import "ui/message_center/cocoa/popup_collection.h"
#include "ui/message_center/message_center.h"
// static
std::unique_ptr<PopupsOnlyUiController::Delegate>
PopupsOnlyUiController::CreateDelegate() {
return std::make_unique<MessageCenterBridge>(
message_center::MessageCenter::Get());
}
MessageCenterBridge::MessageCenterBridge(
message_center::MessageCenter* message_center)
: message_center_(message_center) {}
MessageCenterBridge::~MessageCenterBridge() {}
void MessageCenterBridge::ShowPopups() {
popup_collection_.reset(
[[MCPopupCollection alloc] initWithMessageCenter:message_center_]);
}
void MessageCenterBridge::HidePopups() {
popup_collection_.reset();
}
...@@ -116,10 +116,6 @@ jumbo_component("base") { ...@@ -116,10 +116,6 @@ jumbo_component("base") {
"cocoa/focus_tracker.mm", "cocoa/focus_tracker.mm",
"cocoa/focus_window_set.h", "cocoa/focus_window_set.h",
"cocoa/focus_window_set.mm", "cocoa/focus_window_set.mm",
"cocoa/hover_button.h",
"cocoa/hover_button.mm",
"cocoa/hover_image_button.h",
"cocoa/hover_image_button.mm",
"cocoa/menu_controller.h", "cocoa/menu_controller.h",
"cocoa/menu_controller.mm", "cocoa/menu_controller.mm",
"cocoa/nib_loading.h", "cocoa/nib_loading.h",
...@@ -851,8 +847,6 @@ test("ui_base_unittests") { ...@@ -851,8 +847,6 @@ test("ui_base_unittests") {
"cocoa/cocoa_base_utils_unittest.mm", "cocoa/cocoa_base_utils_unittest.mm",
"cocoa/constrained_window/constrained_window_animation_unittest.mm", "cocoa/constrained_window/constrained_window_animation_unittest.mm",
"cocoa/focus_tracker_unittest.mm", "cocoa/focus_tracker_unittest.mm",
"cocoa/hover_button_unittest.mm",
"cocoa/hover_image_button_unittest.mm",
"cocoa/menu_controller_unittest.mm", "cocoa/menu_controller_unittest.mm",
"cocoa/touch_bar_util_unittest.mm", "cocoa/touch_bar_util_unittest.mm",
"cocoa/tracking_area_unittest.mm", "cocoa/tracking_area_unittest.mm",
......
// Copyright (c) 2011 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.
#ifndef UI_BASE_COCOA_HOVER_BUTTON_
#define UI_BASE_COCOA_HOVER_BUTTON_
#import <Cocoa/Cocoa.h>
#import "ui/base/cocoa/tracking_area.h"
#import "ui/base/ui_base_export.h"
@class HoverButtonCocoa;
// Assign an object which conforms to this protocol to a HoverButtonCocoa's
// dragDelegate property to make the button draggable.
UI_BASE_EXPORT
@protocol HoverButtonDragDelegate
// When the user performs a drag on the HoverButtonCocoa, this method will be
// called with the button and the mouse down event. The delegate is expected to
// begin a drag by calling -[NSView beginDraggingSessionWithItems:event:source:]
// with the event or run a nested tracking loop. When it returns, the
// HoverButtonCocoa returns to kHoverStateNone and stops tracking the mouse.
- (void)beginDragFromHoverButton:(HoverButtonCocoa*)button
event:(NSEvent*)event;
@end
// A button that changes when you hover over it and click it.
UI_BASE_EXPORT
@interface HoverButtonCocoa : NSButton {
@protected
// Enumeration of the hover states that the close button can be in at any one
// time. The button cannot be in more than one hover state at a time.
enum CloseButtonHoverState {
kHoverStateNone = 0,
kHoverStateMouseOver = 1,
kHoverStateMouseDown = 2
};
CloseButtonHoverState hoverState_;
@private
// Tracking area for button mouseover states. Nil if not enabled.
ui::ScopedCrTrackingArea trackingArea_;
BOOL mouseDown_;
BOOL sendActionOnMouseDown_;
}
@property(nonatomic) CloseButtonHoverState hoverState;
// Enables or disables the tracking for the button.
@property(nonatomic) BOOL trackingEnabled;
// Assign an object to make the button a drag source.
@property(nonatomic, assign) id<HoverButtonDragDelegate> dragDelegate;
// Enables or disables sending the action on mouse down event.
@property(nonatomic) BOOL sendActionOnMouseDown;
// An NSRect in the view's coordinate space which is used for hover and hit
// testing. Default value is NSZeroRect, which makes the hitbox equal to the
// view's bounds. May be overridden by subclasses. Example: A button in the
// corner of a fullscreen window might extend its hitbox to the edges of the
// window so that it can be clicked more easily (Fitts's law).
@property(readonly, nonatomic) NSRect hitbox;
// Common initialization called from initWithFrame: and awakeFromNib.
// Subclassers should call [super commonInit].
- (void)commonInit;
// Text that would be announced by screen readers.
- (void)setAccessibilityTitle:(NSString*)accessibilityTitle;
// Checks to see whether the mouse is in the button's bounds and update
// the image in case it gets out of sync. This occurs to the close button
// when you close a tab so the tab to the left of it takes its place, and
// drag the button without moving the mouse before you press the button down.
- (void)checkImageState;
@end
#endif // UI_BASE_COCOA_HOVER_BUTTON_
// Copyright (c) 2010 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.
#import "ui/base/cocoa/hover_button.h"
#include <cmath>
namespace {
// Distance to start a drag when a dragDelegate is assigned.
constexpr CGFloat kDragDistance = 5;
} // namespace
@implementation HoverButtonCocoa
@synthesize hoverState = hoverState_;
@synthesize trackingEnabled = trackingEnabled_;
@synthesize dragDelegate = dragDelegate_;
@synthesize sendActionOnMouseDown = sendActionOnMouseDown_;
- (instancetype)initWithFrame:(NSRect)frameRect {
if ((self = [super initWithFrame:frameRect])) {
[self commonInit];
}
return self;
}
- (void)awakeFromNib {
[self commonInit];
}
- (void)commonInit {
self.hoverState = kHoverStateNone;
self.trackingEnabled = YES;
}
- (void)dealloc {
self.trackingEnabled = NO;
[super dealloc];
}
- (NSRect)hitbox {
return NSZeroRect;
}
- (void)setTrackingEnabled:(BOOL)trackingEnabled {
if (trackingEnabled == trackingEnabled_)
return;
trackingEnabled_ = trackingEnabled;
[self updateTrackingAreas];
}
- (void)setEnabled:(BOOL)enabled {
if (enabled == self.enabled)
return;
super.enabled = enabled;
[self updateTrackingAreas];
}
- (void)mouseEntered:(NSEvent*)theEvent {
if (trackingArea_.get())
self.hoverState = kHoverStateMouseOver;
}
- (void)mouseMoved:(NSEvent*)theEvent {
[self checkImageState];
}
- (void)mouseExited:(NSEvent*)theEvent {
if (trackingArea_.get())
self.hoverState = kHoverStateNone;
}
- (void)mouseDown:(NSEvent*)theEvent {
if (!self.enabled)
return;
mouseDown_ = YES;
self.hoverState = kHoverStateMouseDown;
if (sendActionOnMouseDown_)
[self sendAction:self.action to:self.target];
// The hover button needs to hold onto itself here for a bit. Otherwise,
// it can be freed while in the tracking loop below.
// http://crbug.com/28220
base::scoped_nsobject<HoverButtonCocoa> myself([self retain]);
// Begin tracking the mouse.
if ([theEvent type] == NSLeftMouseDown) {
NSWindow* window = [self window];
NSEvent* nextEvent = nil;
// For the tracking loop ignore key events so that they don't pile up in
// the queue and get processed after the user releases the mouse.
const NSEventMask eventMask = (NSLeftMouseDraggedMask | NSLeftMouseUpMask |
NSKeyDownMask | NSKeyUpMask);
while ((nextEvent = [window nextEventMatchingMask:eventMask])) {
if ([nextEvent type] == NSLeftMouseUp)
break;
// Update the image state, which will change if the user moves the mouse
// into or out of the button.
[self checkImageState];
if (dragDelegate_ && [nextEvent type] == NSLeftMouseDragged) {
const NSPoint startPos = [theEvent locationInWindow];
const NSPoint pos = [nextEvent locationInWindow];
if (std::abs(startPos.x - pos.x) > kDragDistance ||
std::abs(startPos.y - pos.y) > kDragDistance) {
[dragDelegate_ beginDragFromHoverButton:self event:nextEvent];
mouseDown_ = NO;
self.hoverState = kHoverStateNone;
return;
}
}
}
}
// If the mouse is still over the button, it means the user clicked the
// button.
if (!sendActionOnMouseDown_ && self.hoverState == kHoverStateMouseDown) {
[self sendAction:self.action to:self.target];
}
// Clean up.
mouseDown_ = NO;
[self checkImageState];
}
- (void)setAccessibilityTitle:(NSString*)accessibilityTitle {
NSCell* cell = [self cell];
[cell accessibilitySetOverrideValue:accessibilityTitle
forAttribute:NSAccessibilityTitleAttribute];
}
- (void)updateTrackingAreas {
if (trackingEnabled_ && self.enabled) {
NSRect hitbox = self.hitbox;
if (CrTrackingArea* trackingArea = trackingArea_.get()) {
if (NSEqualRects(trackingArea.rect, hitbox))
return;
[self removeTrackingArea:trackingArea];
}
trackingArea_.reset([[CrTrackingArea alloc]
initWithRect:hitbox
options:NSTrackingMouseEnteredAndExited |
NSTrackingMouseMoved |
NSTrackingActiveAlways |
(NSIsEmptyRect(hitbox) ? NSTrackingInVisibleRect : 0)
owner:self
userInfo:nil]);
[self addTrackingArea:trackingArea_.get()];
// If you have a separate window that overlaps the close button, and you
// move the mouse directly over the close button without entering another
// part of the tab strip, we don't get any mouseEntered event since the
// tracking area was disabled when we entered.
// Done with a delay of 0 because sometimes an event appears to be missed
// between the activation of the tracking area and the call to
// checkImageState resulting in the button state being incorrect.
[self performSelector:@selector(checkImageState)
withObject:nil
afterDelay:0];
} else {
if (trackingArea_.get()) {
self.hoverState = kHoverStateNone;
[self removeTrackingArea:trackingArea_.get()];
trackingArea_.reset(nil);
}
}
[super updateTrackingAreas];
[self checkImageState];
}
- (void)checkImageState {
if (!trackingArea_.get())
return;
NSEvent* currentEvent = [NSApp currentEvent];
if (!currentEvent || currentEvent.window != self.window)
return;
// Update the button's state if the button has moved.
const NSPoint mouseLoc =
[self.superview convertPoint:currentEvent.locationInWindow fromView:nil];
BOOL mouseInBounds = [self hitTest:mouseLoc] != nil;
if (mouseDown_ && mouseInBounds) {
self.hoverState = kHoverStateMouseDown;
} else {
self.hoverState = mouseInBounds ? kHoverStateMouseOver : kHoverStateNone;
}
}
- (void)setHoverState:(CloseButtonHoverState)hoverState {
if (hoverState == hoverState_)
return;
hoverState_ = hoverState;
self.needsDisplay = YES;
}
- (NSView*)hitTest:(NSPoint)point {
if (NSPointInRect([self.superview convertPoint:point toView:self],
self.hitbox)) {
return self;
}
return [super hitTest:point];
}
@end
// Copyright 2017 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.
#import "ui/base/cocoa/hover_button.h"
#import <Cocoa/Cocoa.h>
#import "ui/base/test/cocoa_helper.h"
#import "ui/events/test/cocoa_test_event_utils.h"
@interface TestHoverButton : HoverButtonCocoa
@property(readwrite, nonatomic) NSRect hitbox;
@end
@implementation TestHoverButton
@synthesize hitbox = hitbox_;
- (void)setHitbox:(NSRect)hitbox {
hitbox_ = hitbox;
[self updateTrackingAreas];
}
@end
@interface HoverButtonTestTarget : NSObject
@property(nonatomic, copy) void (^actionHandler)(id);
@end
@implementation HoverButtonTestTarget
@synthesize actionHandler = actionHandler_;
- (void)dealloc {
[actionHandler_ release];
[super dealloc];
}
- (IBAction)action:(id)sender {
actionHandler_(sender);
}
@end
@interface HoverButtonTestDragDelegate : NSObject<HoverButtonDragDelegate>
@property(nonatomic, copy) void (^dragHandler)(HoverButtonCocoa*, NSEvent*);
@end
@implementation HoverButtonTestDragDelegate
@synthesize dragHandler = dragHandler_;
- (void)dealloc {
[dragHandler_ release];
[super dealloc];
}
- (void)beginDragFromHoverButton:(HoverButtonCocoa*)button
event:(NSEvent*)event {
dragHandler_(button, event);
}
@end
namespace {
class HoverButtonTest : public ui::CocoaTest {
public:
HoverButtonTest() {
NSRect frame = NSMakeRect(0, 0, 20, 20);
base::scoped_nsobject<TestHoverButton> button(
[[TestHoverButton alloc] initWithFrame:frame]);
button_ = button;
target_.reset([[HoverButtonTestTarget alloc] init]);
button_.target = target_;
button_.action = @selector(action:);
[[test_window() contentView] addSubview:button_];
}
protected:
void HoverAndExpect(CloseButtonHoverState hoverState) {
EXPECT_EQ(kHoverStateNone, button_.hoverState);
[button_ mouseEntered:cocoa_test_event_utils::EnterEvent()];
EXPECT_EQ(hoverState, button_.hoverState);
[button_ mouseExited:cocoa_test_event_utils::ExitEvent()];
EXPECT_EQ(kHoverStateNone, button_.hoverState);
}
bool HandleMouseDown(NSEvent* mouseDownEvent) {
__block bool action_sent = false;
target_.get().actionHandler = ^(id sender) {
action_sent = true;
EXPECT_EQ(kHoverStateMouseDown, button_.hoverState);
};
[NSApp sendEvent:mouseDownEvent];
target_.get().actionHandler = nil;
return action_sent;
}
TestHoverButton* button_; // Weak, owned by test_window().
base::scoped_nsobject<HoverButtonTestTarget> target_;
};
TEST_VIEW(HoverButtonTest, button_)
TEST_F(HoverButtonTest, Hover) {
EXPECT_EQ(kHoverStateNone, button_.hoverState);
// Default
HoverAndExpect(kHoverStateMouseOver);
// Tracking disabled
button_.trackingEnabled = NO;
HoverAndExpect(kHoverStateNone);
button_.trackingEnabled = YES;
// Button disabled
button_.enabled = NO;
HoverAndExpect(kHoverStateNone);
button_.enabled = YES;
// Back to normal
HoverAndExpect(kHoverStateMouseOver);
}
TEST_F(HoverButtonTest, Click) {
EXPECT_EQ(kHoverStateNone, button_.hoverState);
const auto click = cocoa_test_event_utils::MouseClickInView(button_, 1);
[NSApp postEvent:click.second atStart:YES];
EXPECT_TRUE(HandleMouseDown(click.first));
button_.enabled = NO;
EXPECT_FALSE(HandleMouseDown(click.first));
EXPECT_EQ(kHoverStateNone, button_.hoverState);
}
TEST_F(HoverButtonTest, CustomHitbox) {
NSRect hitbox = button_.frame;
hitbox.size.width += 10;
NSPoint inside_hit_point =
NSMakePoint(NSMaxX(button_.frame) + 5, NSMidY(button_.frame));
NSPoint outside_hit_point =
NSMakePoint(inside_hit_point.x + 10, inside_hit_point.y);
{
NSRect trackingRect = button_.trackingAreas[0].rect;
EXPECT_FALSE(NSPointInRect(inside_hit_point, trackingRect));
EXPECT_FALSE(NSPointInRect(outside_hit_point, trackingRect));
EXPECT_NE(button_, [button_ hitTest:inside_hit_point]);
EXPECT_EQ(nil, [button_ hitTest:outside_hit_point]);
}
button_.hitbox = hitbox;
{
NSRect trackingRect = button_.trackingAreas[0].rect;
EXPECT_TRUE(NSPointInRect(inside_hit_point, trackingRect));
EXPECT_FALSE(NSPointInRect(outside_hit_point, trackingRect));
EXPECT_EQ(button_, [button_ hitTest:inside_hit_point]);
EXPECT_EQ(nil, [button_ hitTest:outside_hit_point]);
}
button_.hitbox = NSZeroRect;
{
NSRect trackingRect = button_.trackingAreas[0].rect;
EXPECT_FALSE(NSPointInRect(inside_hit_point, trackingRect));
EXPECT_FALSE(NSPointInRect(outside_hit_point, trackingRect));
EXPECT_NE(button_, [button_ hitTest:inside_hit_point]);
EXPECT_EQ(nil, [button_ hitTest:outside_hit_point]);
}
}
TEST_F(HoverButtonTest, DragDelegate) {
base::scoped_nsobject<HoverButtonTestDragDelegate> dragDelegate(
[[HoverButtonTestDragDelegate alloc] init]);
__block bool dragged = false;
dragDelegate.get().dragHandler = ^(HoverButtonCocoa* button, NSEvent* event) {
dragged = true;
};
button_.dragDelegate = dragDelegate;
const auto click = cocoa_test_event_utils::MouseClickInView(button_, 1);
NSPoint targetPoint = click.first.locationInWindow;
targetPoint.x += 5; // *Not* enough to trigger a drag.
[NSApp postEvent:cocoa_test_event_utils::MouseEventAtPointInWindow(
targetPoint, NSEventTypeLeftMouseDragged,
[button_ window], 1)
atStart:NO];
[NSApp postEvent:click.second atStart:NO];
EXPECT_TRUE(HandleMouseDown(click.first));
EXPECT_FALSE(dragged);
targetPoint.x += 1; // Now it's enough to trigger a drag.
[NSApp postEvent:cocoa_test_event_utils::MouseEventAtPointInWindow(
targetPoint, NSEventTypeLeftMouseDragged,
[button_ window], 1)
atStart:NO];
[NSApp postEvent:click.second atStart:NO];
EXPECT_FALSE(HandleMouseDown(click.first));
EXPECT_TRUE(dragged);
}
} // namespace
// Copyright (c) 2011 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.
#ifndef UI_BASE_COCOA_HOVER_IMAGE_BUTTON_H_
#define UI_BASE_COCOA_HOVER_IMAGE_BUTTON_H_
#import <Cocoa/Cocoa.h>
#import "base/mac/scoped_nsobject.h"
#import "ui/base/cocoa/hover_button.h"
#include "ui/base/ui_base_export.h"
// A button that changes images when you hover over it and click it.
UI_BASE_EXPORT
@interface HoverImageButton : HoverButtonCocoa {
@private
base::scoped_nsobject<NSImage> defaultImage_;
base::scoped_nsobject<NSImage> hoverImage_;
base::scoped_nsobject<NSImage> pressedImage_;
}
// Disables a click within the button from activating the application.
@property(nonatomic) BOOL disableActivationOnClick;
// Sets the default image.
- (void)setDefaultImage:(NSImage*)image;
// Sets the hover image.
- (void)setHoverImage:(NSImage*)image;
// Sets the pressed image.
- (void)setPressedImage:(NSImage*)image;
@end
#endif // UI_BASE_COCOA_HOVER_IMAGE_BUTTON_H_
// Copyright (c) 2011 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.
#import "ui/base/cocoa/hover_image_button.h"
@implementation HoverImageButton
@synthesize disableActivationOnClick = disableActivationOnClick_;
- (void)drawRect:(NSRect)rect {
if (hoverState_ == kHoverStateMouseDown && pressedImage_) {
[super setImage:pressedImage_.get()];
} else if (hoverState_ == kHoverStateMouseOver && hoverImage_) {
[super setImage:hoverImage_.get()];
} else {
[super setImage:defaultImage_.get()];
}
[super drawRect:rect];
}
- (void)setDefaultImage:(NSImage*)image {
defaultImage_.reset([image retain]);
}
- (void)setHoverImage:(NSImage*)image {
hoverImage_.reset([image retain]);
}
- (void)setPressedImage:(NSImage*)image {
pressedImage_.reset([image retain]);
}
- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent*)theEvent {
// To avoid activating the app on a click inside the button, first tell
// the Appkit not to immediately order the HoverImageButton's window front in
// response to theEvent.
return disableActivationOnClick_;
}
- (void)mouseDown:(NSEvent*)mouseDownEvent {
// If disabling activation on click, tell the Appkit to cancel window ordering
// for this mouse down.
if (disableActivationOnClick_) {
[[NSApplication sharedApplication] preventWindowOrdering];
}
[super mouseDown:mouseDownEvent];
}
@end
// Copyright (c) 2012 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.
#import "ui/base/cocoa/hover_image_button.h"
#import "base/mac/scoped_nsobject.h"
#import "ui/base/test/cocoa_helper.h"
#include "ui/events/test/cocoa_test_event_utils.h"
namespace {
class HoverImageButtonTest : public ui::CocoaTest {
public:
HoverImageButtonTest() {
NSRect content_frame = [[test_window() contentView] frame];
base::scoped_nsobject<HoverImageButton> button(
[[HoverImageButton alloc] initWithFrame:content_frame]);
button_ = button.get();
[[test_window() contentView] addSubview:button_];
}
void DrawRect() {
[button_ lockFocus];
[button_ drawRect:[button_ bounds]];
[button_ unlockFocus];
}
HoverImageButton* button_;
};
// Test mouse events.
TEST_F(HoverImageButtonTest, ImageSwap) {
NSImage* image = [NSImage imageNamed:NSImageNameStatusAvailable];
NSImage* hover = [NSImage imageNamed:NSImageNameStatusNone];
[button_ setDefaultImage:image];
[button_ setHoverImage:hover];
[button_ mouseEntered:cocoa_test_event_utils::EnterEvent()];
DrawRect();
EXPECT_EQ([button_ image], hover);
[button_ mouseExited:cocoa_test_event_utils::ExitEvent()];
DrawRect();
EXPECT_NE([button_ image], hover);
EXPECT_EQ([button_ image], image);
}
} // namespace
...@@ -62,14 +62,6 @@ jumbo_component("message_center") { ...@@ -62,14 +62,6 @@ jumbo_component("message_center") {
"//build/config/compiler:no_size_t_to_int_warning", "//build/config/compiler:no_size_t_to_int_warning",
] ]
sources = [ sources = [
"cocoa/notification_controller.h",
"cocoa/notification_controller.mm",
"cocoa/opaque_views.h",
"cocoa/opaque_views.mm",
"cocoa/popup_collection.h",
"cocoa/popup_collection.mm",
"cocoa/popup_controller.h",
"cocoa/popup_controller.mm",
"lock_screen/empty_lock_screen_controller.cc", "lock_screen/empty_lock_screen_controller.cc",
"lock_screen/empty_lock_screen_controller.h", "lock_screen/empty_lock_screen_controller.h",
"lock_screen/lock_screen_controller.h", "lock_screen/lock_screen_controller.h",
...@@ -103,13 +95,6 @@ jumbo_component("message_center") { ...@@ -103,13 +95,6 @@ jumbo_component("message_center") {
] ]
} }
if (is_mac) {
libs = [
"AppKit.framework",
"Foundation.framework",
]
}
if (toolkit_views) { if (toolkit_views) {
sources += [ sources += [
"views/bounded_label.cc", "views/bounded_label.cc",
...@@ -193,9 +178,6 @@ if (enable_message_center) { ...@@ -193,9 +178,6 @@ if (enable_message_center) {
test("message_center_unittests") { test("message_center_unittests") {
sources = [ sources = [
"cocoa/notification_controller_unittest.mm",
"cocoa/popup_collection_unittest.mm",
"cocoa/popup_controller_unittest.mm",
"lock_screen/fake_lock_screen_controller.cc", "lock_screen/fake_lock_screen_controller.cc",
"lock_screen/fake_lock_screen_controller.h", "lock_screen/fake_lock_screen_controller.h",
"message_center_impl_unittest.cc", "message_center_impl_unittest.cc",
...@@ -242,10 +224,6 @@ if (enable_message_center) { ...@@ -242,10 +224,6 @@ if (enable_message_center) {
] ]
} }
if (is_mac) {
deps += [ "//ui/gfx:test_support" ]
}
if (toolkit_views) { if (toolkit_views) {
sources += [ sources += [
"views/bounded_label_unittest.cc", "views/bounded_label_unittest.cc",
......
// Copyright (c) 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.
#ifndef UI_MESSAGE_CENTER_COCOA_NOTIFICATION_CONTROLLER_H_
#define UI_MESSAGE_CENTER_COCOA_NOTIFICATION_CONTROLLER_H_
#import <Cocoa/Cocoa.h>
#include <string>
#import "base/mac/scoped_nsobject.h"
#include "ui/message_center/message_center_export.h"
namespace message_center {
class MessageCenter;
class Notification;
}
@class HoverImageButton;
namespace message_center {
// A struct that can hold all the temporary frames
// created when adjusting a view
struct NotificationLayoutParams {
NSRect rootFrame;
NSRect titleFrame;
NSRect messageFrame;
NSRect contextMessageFrame;
NSRect settingsButtonFrame;
NSRect listFrame;
NSRect progressBarFrame;
};
}
// The base view controller class for notifications. A notification at minimum
// has an image, title, body, and close button. This controller can be used as
// the content for both a popup bubble and a view in the notification tray.
MESSAGE_CENTER_EXPORT
@interface MCNotificationController : NSViewController {
@protected
// The message object. Weak.
const message_center::Notification* notification_;
// A copy of the notification ID.
std::string notificationID_;
// Controller of the notifications, where action messages are forwarded. Weak.
message_center::MessageCenter* messageCenter_;
// The button that invokes |-close:|, in the upper-right corner.
base::scoped_nsobject<HoverImageButton> closeButton_;
// The button that invokes |-settingsClicked:|, in the bottom right corner of
// the context message.
base::scoped_nsobject<HoverImageButton> settingsButton_;
// The small icon associated with the notification, on the bottom right.
base::scoped_nsobject<NSImageView> smallImage_;
// The large icon associated with the notification, on the left side.
base::scoped_nsobject<NSImageView> icon_;
// The title of the message.
base::scoped_nsobject<NSTextView> title_;
// Body text of the message. Hidden for list notifications.
base::scoped_nsobject<NSTextView> message_;
// Context-giving text of the message. Alternate font used to distinguish it.
base::scoped_nsobject<NSTextView> contextMessage_;
// Container for optional list view that contains multiple items.
base::scoped_nsobject<NSView> listView_;
// Container for optional progress bar view.
base::scoped_nsobject<NSProgressIndicator> progressBarView_;
// Container for optional items at the bottom of the notification.
base::scoped_nsobject<NSView> bottomView_;
}
// Creates a new controller for a given notification.
- (id)initWithNotification:(const message_center::Notification*)notification
messageCenter:(message_center::MessageCenter*)messageCenter;
// If the model object changes, this method will update the views to reflect
// the new model object. Returns the updated frame of the notification.
- (NSRect)updateNotification:(const message_center::Notification*)notification;
// Action for clicking on the notification's |closeButton_|.
- (void)close:(id)sender;
// Action for clicking on the notification's |settingsButton_|.
- (void)settingsClicked:(id)sender;
// Accessor for the notification.
- (const message_center::Notification*)notification;
// Gets the notification ID. This string is owned by the NotificationController
// rather than the model object, so it's safe to use after the Notification has
// been deleted.
- (const std::string&)notificationID;
// Called when the user clicks within the notification view.
- (void)notificationClicked;
// Adjust the position and height of all the internal frames by |delta|.
- (void)adjustFrameHeight:(message_center::NotificationLayoutParams*)frames
delta:(CGFloat)delta;
@end
@interface MCNotificationController (TestingInterface)
- (NSImageView*)smallImageView;
- (NSImageView*)iconView;
@end
#endif // UI_MESSAGE_CENTER_COCOA_NOTIFICATION_CONTROLLER_H_
// Copyright (c) 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.
#import "ui/message_center/cocoa/notification_controller.h"
#include <stddef.h>
#include <algorithm>
#include "base/mac/foundation_util.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/url_formatter/elide_url.h"
#include "skia/ext/skia_utils_mac.h"
#import "ui/base/cocoa/hover_image_button.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"
#include "url/gurl.h"
@interface MCNotificationProgressBar : NSProgressIndicator
@end
@implementation MCNotificationProgressBar
- (void)drawRect:(NSRect)dirtyRect {
NSRect sliceRect, remainderRect;
double progressFraction = ([self doubleValue] - [self minValue]) /
([self maxValue] - [self minValue]);
NSDivideRect(dirtyRect, &sliceRect, &remainderRect,
NSWidth(dirtyRect) * progressFraction, NSMinXEdge);
NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:dirtyRect
xRadius:message_center::kProgressBarCornerRadius
yRadius:message_center::kProgressBarCornerRadius];
[skia::SkColorToCalibratedNSColor(message_center::kProgressBarBackgroundColor)
set];
[path fill];
if (progressFraction == 0.0)
return;
path = [NSBezierPath bezierPathWithRoundedRect:sliceRect
xRadius:message_center::kProgressBarCornerRadius
yRadius:message_center::kProgressBarCornerRadius];
[skia::SkColorToCalibratedNSColor(message_center::kProgressBarSliceColor)
set];
[path fill];
}
- (id)accessibilityAttributeValue:(NSString*)attribute {
double progressValue = 0.0;
if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) {
progressValue = [self doubleValue];
} else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) {
progressValue = [self minValue];
} else if ([attribute isEqualToString:NSAccessibilityMaxValueAttribute]) {
progressValue = [self maxValue];
} else {
return [super accessibilityAttributeValue:attribute];
}
return [NSString stringWithFormat:@"%lf", progressValue];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface MCNotificationButton : NSButton
@end
@implementation MCNotificationButton
// drawRect: needs to fill the button with a background, otherwise we don't get
// subpixel antialiasing.
- (void)drawRect:(NSRect)dirtyRect {
NSColor* color = skia::SkColorToCalibratedNSColor(
message_center::kNotificationBackgroundColor);
[color set];
NSRectFill(dirtyRect);
[super drawRect:dirtyRect];
}
@end
@interface MCNotificationButtonCell : NSButtonCell {
BOOL hovered_;
}
@end
////////////////////////////////////////////////////////////////////////////////
@implementation MCNotificationButtonCell
- (BOOL)isOpaque {
return YES;
}
- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView*)controlView {
// Else mouseEntered: and mouseExited: won't be called and hovered_ won't be
// valid.
DCHECK([self showsBorderOnlyWhileMouseInside]);
if (!hovered_)
return;
[skia::SkColorToCalibratedNSColor(
message_center::kHoveredButtonBackgroundColor) set];
NSRectFill(frame);
}
- (void)drawImage:(NSImage*)image
withFrame:(NSRect)frame
inView:(NSView*)controlView {
if (!image)
return;
NSRect rect = NSMakeRect(message_center::kButtonHorizontalPadding,
message_center::kButtonIconTopPadding,
message_center::kNotificationButtonIconSize,
message_center::kNotificationButtonIconSize);
[image drawInRect:rect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
}
- (NSRect)drawTitle:(NSAttributedString*)title
withFrame:(NSRect)frame
inView:(NSView*)controlView {
CGFloat offsetX = message_center::kButtonHorizontalPadding;
if ([base::mac::ObjCCastStrict<NSButton>(controlView) image]) {
offsetX += message_center::kNotificationButtonIconSize +
message_center::kButtonIconToTitlePadding;
}
frame.origin.x = offsetX;
frame.size.width -= offsetX;
NSDictionary* attributes = @{
NSFontAttributeName :
[title attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL],
NSForegroundColorAttributeName :
skia::SkColorToCalibratedNSColor(message_center::kRegularTextColor),
};
[[title string] drawWithRect:frame
options:(NSStringDrawingUsesLineFragmentOrigin |
NSStringDrawingTruncatesLastVisibleLine)
attributes:attributes];
return frame;
}
- (void)mouseEntered:(NSEvent*)event {
hovered_ = YES;
// Else the cell won't be repainted on hover.
[super mouseEntered:event];
}
- (void)mouseExited:(NSEvent*)event {
hovered_ = NO;
[super mouseExited:event];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface MCNotificationView : NSBox {
@private
MCNotificationController* controller_;
}
- (id)initWithController:(MCNotificationController*)controller
frame:(NSRect)frame;
@end
@implementation MCNotificationView
- (id)initWithController:(MCNotificationController*)controller
frame:(NSRect)frame {
if ((self = [super initWithFrame:frame]))
controller_ = controller;
return self;
}
- (void)mouseUp:(NSEvent*)event {
if (event.type != NSLeftMouseUp) {
[super mouseUp:event];
return;
}
if (NSPointInRect([self convertPoint:event.locationInWindow fromView:nil],
self.bounds)) {
[controller_ notificationClicked];
}
}
- (NSView*)hitTest:(NSPoint)point {
// Route the mouse click events on NSTextView to the container view.
NSView* hitView = [super hitTest:point];
if (hitView)
return [hitView isKindOfClass:[NSTextView class]] ? self : hitView;
return nil;
}
- (BOOL)accessibilityIsIgnored {
return NO;
}
- (NSArray*)accessibilityActionNames {
return @[ NSAccessibilityPressAction ];
}
- (void)accessibilityPerformAction:(NSString*)action {
if ([action isEqualToString:NSAccessibilityPressAction]) {
[controller_ notificationClicked];
return;
}
[super accessibilityPerformAction:action];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface AccessibilityIgnoredBox : NSBox
@end
// Ignore this element, but expose its children to accessibility.
@implementation AccessibilityIgnoredBox
- (BOOL)accessibilityIsIgnored {
return YES;
}
// Pretend this element has no children.
// TODO(petewil): Until we have alt text available, we will hide the children of
// the box also. Remove this override once alt text is set (by using
// NSAccessibilityDescriptionAttribute).
- (id)accessibilityAttributeValue:(NSString*)attribute {
// If we get a request for NSAccessibilityChildrenAttribute, return an empty
// array to pretend we have no children.
if ([attribute isEqualToString:NSAccessibilityChildrenAttribute])
return @[];
else
return [super accessibilityAttributeValue:attribute];
}
@end
////////////////////////////////////////////////////////////////////////////////
@interface MCNotificationController (Private)
// Configures a NSBox to be borderless, titleless, and otherwise appearance-
// free.
- (void)configureCustomBox:(NSBox*)box;
// Initializes the icon_ ivar and returns the view to insert into the hierarchy.
- (NSView*)createIconView;
// Creates a box that shows a border when the icon is not big enough to fill the
// space.
- (NSBox*)createImageBox:(const gfx::Image&)notificationImage;
// Initializes the closeButton_ ivar with the configured button.
- (void)configureCloseButtonInFrame:(NSRect)rootFrame;
// Initializes the settingsButton_ ivar with the configured button.
- (void)configureSettingsButtonInFrame:(NSRect)rootFrame;
// Creates the smallImage_ ivar with the appropriate frame.
- (NSView*)createSmallImageInFrame:(NSRect)rootFrame;
// Initializes title_ in the given frame.
- (void)configureTitleInFrame:(NSRect)rootFrame;
// Initializes message_ in the given frame.
- (void)configureBodyInFrame:(NSRect)rootFrame;
// Initializes contextMessage_ in the given frame.
- (void)configureContextMessageInFrame:(NSRect)rootFrame;
// Creates a NSTextView that the caller owns configured as a label in a
// notification.
- (NSTextView*)newLabelWithFrame:(NSRect)frame;
// Gets the rectangle in which notification content should be placed. This
// rectangle is to the right of the icon and left of the control buttons.
// This depends on the icon_ and closeButton_ being initialized.
- (NSRect)currentContentRect;
// Returns the wrapped text that could fit within the content rect with not
// more than the given number of lines. The wrapped text would be painted using
// the given font. The Ellipsis could be added at the end of the last line if
// it is too long. Outputs the number of lines computed in the actualLines
// parameter.
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)font
maxNumberOfLines:(size_t)lines
actualLines:(size_t*)actualLines;
// Same as above without outputting the lines formatted.
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)font
maxNumberOfLines:(size_t)lines;
@end
////////////////////////////////////////////////////////////////////////////////
@implementation MCNotificationController
- (id)initWithNotification:(const message_center::Notification*)notification
messageCenter:(message_center::MessageCenter*)messageCenter {
if ((self = [super initWithNibName:nil bundle:nil])) {
notification_ = notification;
notificationID_ = notification_->id();
messageCenter_ = messageCenter;
}
return self;
}
- (void)loadView {
// Create the root view of the notification.
NSRect rootFrame = NSMakeRect(0, 0,
message_center::kNotificationPreferredImageWidth,
message_center::kNotificationIconSize);
base::scoped_nsobject<MCNotificationView> rootView(
[[MCNotificationView alloc] initWithController:self frame:rootFrame]);
[self configureCustomBox:rootView];
[rootView setFillColor:skia::SkColorToCalibratedNSColor(
message_center::kNotificationBackgroundColor)];
[self setView:rootView];
[rootView addSubview:[self createIconView]];
// Create the close button.
[self configureCloseButtonInFrame:rootFrame];
[rootView addSubview:closeButton_];
// Create the small image.
[rootView addSubview:[self createSmallImageInFrame:rootFrame]];
// Create the settings button.
if (notification_->should_show_settings_button()) {
[self configureSettingsButtonInFrame:rootFrame];
[rootView addSubview:settingsButton_];
}
NSRect contentFrame = [self currentContentRect];
// Create the title.
[self configureTitleInFrame:contentFrame];
[rootView addSubview:title_];
// Create the message body.
[self configureBodyInFrame:contentFrame];
[rootView addSubview:message_];
// Create the context message body.
[self configureContextMessageInFrame:contentFrame];
[rootView addSubview:contextMessage_];
// Populate the data.
[self updateNotification:notification_];
}
- (NSRect)updateNotification:(const message_center::Notification*)notification {
DCHECK_EQ(notification->id(), notificationID_);
notification_ = notification;
message_center::NotificationLayoutParams layoutParams;
layoutParams.rootFrame =
NSMakeRect(0, 0, message_center::kNotificationPreferredImageWidth,
message_center::kNotificationIconSize);
[smallImage_ setImage:notification_->small_image().AsNSImage()];
// Update the icon.
[icon_ setImage:notification_->icon().AsNSImage()];
// The message_center:: constants are relative to capHeight at the top and
// relative to the baseline at the bottom, but NSTextField uses the full line
// height for its height.
CGFloat titleTopGap =
roundf([[title_ font] ascender] - [[title_ font] capHeight]);
CGFloat titleBottomGap = roundf(fabs([[title_ font] descender]));
CGFloat titlePadding = message_center::kTextTopPadding - titleTopGap;
CGFloat messageTopGap =
roundf([[message_ font] ascender] - [[message_ font] capHeight]);
CGFloat messageBottomGap = roundf(fabs([[message_ font] descender]));
CGFloat messagePadding =
message_center::kTextTopPadding - titleBottomGap - messageTopGap;
CGFloat contextMessageTopGap = roundf(
[[contextMessage_ font] ascender] - [[contextMessage_ font] capHeight]);
CGFloat contextMessagePadding =
message_center::kTextTopPadding - messageBottomGap - contextMessageTopGap;
// Set the title and recalculate the frame.
size_t actualTitleLines = 0;
[title_ setString:base::SysUTF16ToNSString([self
wrapText:notification_->title()
forFont:[title_ font]
maxNumberOfLines:message_center::kMaxTitleLines
actualLines:&actualTitleLines])];
[title_ sizeToFit];
layoutParams.titleFrame = [title_ frame];
layoutParams.titleFrame.origin.y = NSMaxY(layoutParams.rootFrame) -
titlePadding -
NSHeight(layoutParams.titleFrame);
// The number of message lines depends on the number of context message lines
// and the lines within the title, and whether an image exists.
int messageLineLimit = message_center::kMessageExpandedLineLimit;
if (actualTitleLines > 1)
messageLineLimit -= (actualTitleLines - 1) * 2;
if (!notification_->image().IsEmpty()) {
messageLineLimit /= 2;
if (!notification_->context_message().empty() &&
!notification_->UseOriginAsContextMessage())
messageLineLimit -= message_center::kContextMessageLineLimit;
}
if (messageLineLimit < 0)
messageLineLimit = 0;
// Set the message and recalculate the frame.
[message_ setString:base::SysUTF16ToNSString(
[self wrapText:notification_->message()
forFont:[message_ font]
maxNumberOfLines:messageLineLimit])];
[message_ sizeToFit];
layoutParams.messageFrame = [message_ frame];
// If there are list items, then the message_ view should not be displayed.
const std::vector<message_center::NotificationItem>& items =
notification->items();
// If there are list items, don't show the main message. Also if the message
// is empty, mark it as hidden and set 0 height, so it doesn't take up any
// space (size to fit leaves it 15 px tall.
if (items.size() > 0 || notification_->message().empty()) {
[message_ setHidden:YES];
layoutParams.messageFrame.origin.y = layoutParams.titleFrame.origin.y;
layoutParams.messageFrame.size.height = 0;
} else {
[message_ setHidden:NO];
layoutParams.messageFrame.origin.y = NSMinY(layoutParams.titleFrame) -
messagePadding -
NSHeight(layoutParams.messageFrame);
layoutParams.messageFrame.size.height = NSHeight([message_ frame]);
}
// Set the context message and recalculate the frame.
base::string16 message;
if (notification->UseOriginAsContextMessage()) {
gfx::FontList font_list((gfx::Font([message_ font])));
message = url_formatter::ElideHost(notification->origin_url(), font_list,
message_center::kContextMessageViewWidth,
gfx::Typesetter::NATIVE);
} else {
message = notification->context_message();
}
base::string16 elided =
[self wrapText:message
forFont:[contextMessage_ font]
maxNumberOfLines:message_center::kContextMessageLineLimit];
[contextMessage_ setString:base::SysUTF16ToNSString(elided)];
[contextMessage_ sizeToFit];
layoutParams.contextMessageFrame = [contextMessage_ frame];
if (notification->context_message().empty() &&
!notification->UseOriginAsContextMessage()) {
[contextMessage_ setHidden:YES];
layoutParams.contextMessageFrame.origin.y =
layoutParams.messageFrame.origin.y;
layoutParams.contextMessageFrame.size.height = 0;
} else {
[contextMessage_ setHidden:NO];
// If the context message is used as a domain make sure it's placed at the
// bottom of the top section.
CGFloat contextMessageY = NSMinY(layoutParams.messageFrame) -
contextMessagePadding -
NSHeight(layoutParams.contextMessageFrame);
layoutParams.contextMessageFrame.origin.y =
notification->UseOriginAsContextMessage()
? std::min(NSMinY([icon_ frame]) + contextMessagePadding,
contextMessageY)
: contextMessageY;
layoutParams.contextMessageFrame.size.height =
NSHeight([contextMessage_ frame]);
}
// Calculate the settings button position. It is dependent on whether the
// context message aligns or not with the icon.
layoutParams.settingsButtonFrame = [settingsButton_ frame];
layoutParams.settingsButtonFrame.origin.y =
MIN(NSMinY([icon_ frame]) + message_center::kSmallImagePadding,
NSMinY(layoutParams.contextMessageFrame));
// Create the list item views (up to a maximum).
[listView_ removeFromSuperview];
layoutParams.listFrame = NSZeroRect;
if (items.size() > 0) {
layoutParams.listFrame = [self currentContentRect];
layoutParams.listFrame.origin.y = 0;
layoutParams.listFrame.size.height = 0;
listView_.reset([[NSView alloc] initWithFrame:layoutParams.listFrame]);
[listView_ accessibilitySetOverrideValue:NSAccessibilityListRole
forAttribute:NSAccessibilityRoleAttribute];
[listView_
accessibilitySetOverrideValue:NSAccessibilityContentListSubrole
forAttribute:NSAccessibilitySubroleAttribute];
CGFloat y = 0;
NSFont* font = [NSFont systemFontOfSize:message_center::kMessageFontSize];
CGFloat lineHeight = roundf(NSHeight([font boundingRectForFont]));
const int kNumNotifications =
std::min(items.size(), message_center::kNotificationMaximumItems);
for (int i = kNumNotifications - 1; i >= 0; --i) {
NSTextView* itemView = [self
newLabelWithFrame:NSMakeRect(0, y, NSWidth(layoutParams.listFrame),
lineHeight)];
[itemView setFont:font];
// Disable the word-wrap in order to show the text in single line.
[[itemView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
[[itemView textContainer] setWidthTracksTextView:NO];
// Construct the text from the title and message.
base::string16 text =
items[i].title + base::UTF8ToUTF16(" ") + items[i].message;
base::string16 ellidedText =
[self wrapText:text forFont:font maxNumberOfLines:1];
[itemView setString:base::SysUTF16ToNSString(ellidedText)];
// Use dim color for the title part.
NSColor* titleColor =
skia::SkColorToCalibratedNSColor(message_center::kRegularTextColor);
NSRange titleRange = NSMakeRange(
0,
std::min(ellidedText.size(), items[i].title.size()));
[itemView setTextColor:titleColor range:titleRange];
// Use dim color for the message part if it has not been truncated.
if (ellidedText.size() > items[i].title.size() + 1) {
NSColor* messageColor =
skia::SkColorToCalibratedNSColor(message_center::kDimTextColor);
NSRange messageRange = NSMakeRange(
items[i].title.size() + 1,
ellidedText.size() - items[i].title.size() - 1);
[itemView setTextColor:messageColor range:messageRange];
}
[listView_ addSubview:itemView];
y += lineHeight;
}
// TODO(thakis): The spacing is not completely right.
CGFloat listTopPadding =
message_center::kTextTopPadding - contextMessageTopGap;
layoutParams.listFrame.size.height = y;
layoutParams.listFrame.origin.y = NSMinY(layoutParams.contextMessageFrame) -
listTopPadding -
NSHeight(layoutParams.listFrame);
[listView_ setFrame:layoutParams.listFrame];
[[self view] addSubview:listView_];
}
// Create the progress bar view if needed.
[progressBarView_ removeFromSuperview];
layoutParams.progressBarFrame = NSZeroRect;
if (notification->type() == message_center::NOTIFICATION_TYPE_PROGRESS) {
layoutParams.progressBarFrame = [self currentContentRect];
layoutParams.progressBarFrame.origin.y =
NSMinY(layoutParams.contextMessageFrame) -
message_center::kProgressBarTopPadding -
message_center::kProgressBarThickness;
layoutParams.progressBarFrame.size.height =
message_center::kProgressBarThickness;
progressBarView_.reset([[MCNotificationProgressBar alloc]
initWithFrame:layoutParams.progressBarFrame]);
// Setting indeterminate to NO does not work with custom drawRect.
[progressBarView_ setIndeterminate:YES];
[progressBarView_ setStyle:NSProgressIndicatorBarStyle];
[progressBarView_ setDoubleValue:notification->progress()];
[[self view] addSubview:progressBarView_];
}
// If the bottom-most element so far is out of the rootView's bounds, resize
// the view.
CGFloat minY = NSMinY(layoutParams.contextMessageFrame);
if (listView_ && NSMinY(layoutParams.listFrame) < minY)
minY = NSMinY(layoutParams.listFrame);
if (progressBarView_ && NSMinY(layoutParams.progressBarFrame) < minY)
minY = NSMinY(layoutParams.progressBarFrame);
if (minY < messagePadding) {
CGFloat delta = messagePadding - minY;
[self adjustFrameHeight:&layoutParams delta:delta];
}
// Add the bottom container view.
NSRect frame = layoutParams.rootFrame;
frame.size.height = 0;
[bottomView_ removeFromSuperview];
bottomView_.reset([[NSView alloc] initWithFrame:frame]);
CGFloat y = 0;
// Create action buttons if appropriate, bottom-up.
std::vector<message_center::ButtonInfo> buttons = notification->buttons();
for (int i = buttons.size() - 1; i >= 0; --i) {
message_center::ButtonInfo buttonInfo = buttons[i];
NSRect buttonFrame = frame;
buttonFrame.origin = NSMakePoint(0, y);
buttonFrame.size.height = message_center::kButtonHeight;
base::scoped_nsobject<MCNotificationButton> button(
[[MCNotificationButton alloc] initWithFrame:buttonFrame]);
base::scoped_nsobject<MCNotificationButtonCell> cell(
[[MCNotificationButtonCell alloc]
initTextCell:base::SysUTF16ToNSString(buttonInfo.title)]);
[cell setShowsBorderOnlyWhileMouseInside:YES];
[button setCell:cell];
[button setImage:buttonInfo.icon.AsNSImage()];
[button setBezelStyle:NSSmallSquareBezelStyle];
[button setImagePosition:NSImageLeft];
[button setTag:i];
[button setTarget:self];
[button setAction:@selector(buttonClicked:)];
y += NSHeight(buttonFrame);
frame.size.height += NSHeight(buttonFrame);
[bottomView_ addSubview:button];
NSRect separatorFrame = frame;
separatorFrame.origin = NSMakePoint(0, y);
separatorFrame.size.height = 1;
base::scoped_nsobject<NSBox> separator(
[[AccessibilityIgnoredBox alloc] initWithFrame:separatorFrame]);
[self configureCustomBox:separator];
[separator setFillColor:skia::SkColorToCalibratedNSColor(
message_center::kButtonSeparatorColor)];
y += NSHeight(separatorFrame);
frame.size.height += NSHeight(separatorFrame);
[bottomView_ addSubview:separator];
}
// Create the image view if appropriate.
gfx::Image notificationImage = notification->image();
if (!notificationImage.IsEmpty()) {
NSBox* imageBox = [self createImageBox:notificationImage];
NSRect outerFrame = frame;
outerFrame.origin = NSMakePoint(0, y);
outerFrame.size = [imageBox frame].size;
[imageBox setFrame:outerFrame];
y += NSHeight(outerFrame);
frame.size.height += NSHeight(outerFrame);
[bottomView_ addSubview:imageBox];
}
[bottomView_ setFrame:frame];
[[self view] addSubview:bottomView_];
[self adjustFrameHeight:&layoutParams delta:NSHeight(frame)];
// Make sure that there is a minimum amount of spacing below the icon and
// the edge of the frame.
CGFloat bottomDelta =
NSHeight(layoutParams.rootFrame) - NSHeight([icon_ frame]);
if (bottomDelta > 0 && bottomDelta < message_center::kIconBottomPadding) {
CGFloat bottomAdjust = message_center::kIconBottomPadding - bottomDelta;
[self adjustFrameHeight:&layoutParams delta:bottomAdjust];
}
[[self view] setFrame:layoutParams.rootFrame];
[title_ setFrame:layoutParams.titleFrame];
[message_ setFrame:layoutParams.messageFrame];
[contextMessage_ setFrame:layoutParams.contextMessageFrame];
[settingsButton_ setFrame:layoutParams.settingsButtonFrame];
[listView_ setFrame:layoutParams.listFrame];
[progressBarView_ setFrame:layoutParams.progressBarFrame];
return layoutParams.rootFrame;
}
- (void)close:(id)sender {
[closeButton_ setTarget:nil];
messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
}
- (void)settingsClicked:(id)sender {
[NSApp activateIgnoringOtherApps:YES];
messageCenter_->ClickOnSettingsButton([self notificationID]);
}
- (void)buttonClicked:(id)button {
messageCenter_->ClickOnNotificationButton([self notificationID],
[button tag]);
}
- (const message_center::Notification*)notification {
return notification_;
}
- (const std::string&)notificationID {
return notificationID_;
}
- (void)notificationClicked {
messageCenter_->ClickOnNotification([self notificationID]);
}
- (void)adjustFrameHeight:(message_center::NotificationLayoutParams*)frames
delta:(CGFloat)delta {
frames->rootFrame.size.height += delta;
frames->titleFrame.origin.y += delta;
frames->messageFrame.origin.y += delta;
frames->contextMessageFrame.origin.y += delta;
frames->settingsButtonFrame.origin.y += delta;
frames->listFrame.origin.y += delta;
frames->progressBarFrame.origin.y += delta;
}
// Private /////////////////////////////////////////////////////////////////////
- (void)configureCustomBox:(NSBox*)box {
[box setBoxType:NSBoxCustom];
[box setBorderType:NSNoBorder];
[box setTitlePosition:NSNoTitle];
[box setContentViewMargins:NSZeroSize];
}
- (NSView*)createIconView {
// Create another box that shows a background color when the icon is not
// big enough to fill the space.
NSRect imageFrame = NSMakeRect(0, 0,
message_center::kNotificationIconSize,
message_center::kNotificationIconSize);
base::scoped_nsobject<NSBox> imageBox(
[[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
[self configureCustomBox:imageBox];
[imageBox setAutoresizingMask:NSViewMinYMargin];
// Inside the image box put the actual icon view.
icon_.reset([[NSImageView alloc] initWithFrame:imageFrame]);
[imageBox setContentView:icon_];
return imageBox.autorelease();
}
- (NSBox*)createImageBox:(const gfx::Image&)notificationImage {
using message_center::kNotificationImageBorderSize;
using message_center::kNotificationPreferredImageWidth;
using message_center::kNotificationPreferredImageHeight;
NSRect imageFrame = NSMakeRect(0, 0,
kNotificationPreferredImageWidth,
kNotificationPreferredImageHeight);
base::scoped_nsobject<NSBox> imageBox(
[[AccessibilityIgnoredBox alloc] initWithFrame:imageFrame]);
[self configureCustomBox:imageBox];
[imageBox setFillColor:skia::SkColorToCalibratedNSColor(
message_center::kImageBackgroundColor)];
// Images with non-preferred aspect ratios get a border on all sides.
gfx::Size idealSize = gfx::Size(
kNotificationPreferredImageWidth, kNotificationPreferredImageHeight);
gfx::Size scaledSize = message_center::GetImageSizeForContainerSize(
idealSize, notificationImage.Size());
if (scaledSize != idealSize) {
NSSize borderSize =
NSMakeSize(kNotificationImageBorderSize, kNotificationImageBorderSize);
[imageBox setContentViewMargins:borderSize];
}
NSImage* image = notificationImage.AsNSImage();
base::scoped_nsobject<NSImageView> imageView(
[[NSImageView alloc] initWithFrame:imageFrame]);
[imageView setImage:image];
[imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
[imageBox setContentView:imageView];
return imageBox.autorelease();
}
- (void)configureCloseButtonInFrame:(NSRect)rootFrame {
// The close button is configured to be the same size as the small image.
int closeButtonOriginOffset =
message_center::kSmallImageSize + message_center::kSmallImagePadding;
NSRect closeButtonFrame =
NSMakeRect(NSMaxX(rootFrame) - closeButtonOriginOffset,
NSMaxY(rootFrame) - closeButtonOriginOffset,
message_center::kSmallImageSize,
message_center::kSmallImageSize);
closeButton_.reset([[HoverImageButton alloc] initWithFrame:closeButtonFrame]);
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
[closeButton_ setDefaultImage:
rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE).ToNSImage()];
[closeButton_ setHoverImage:
rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_HOVER).ToNSImage()];
[closeButton_ setPressedImage:
rb.GetNativeImageNamed(IDR_NOTIFICATION_CLOSE_PRESSED).ToNSImage()];
[[closeButton_ cell] setHighlightsBy:NSOnState];
[closeButton_ setTrackingEnabled:YES];
[closeButton_ setBordered:NO];
[closeButton_ setAutoresizingMask:NSViewMinYMargin];
[closeButton_ setTarget:self];
[closeButton_ setAction:@selector(close:)];
[closeButton_ setDisableActivationOnClick:YES];
[[closeButton_ cell]
accessibilitySetOverrideValue:NSAccessibilityCloseButtonSubrole
forAttribute:NSAccessibilitySubroleAttribute];
[[closeButton_ cell]
accessibilitySetOverrideValue:
l10n_util::GetNSString(IDS_APP_ACCNAME_CLOSE)
forAttribute:NSAccessibilityTitleAttribute];
}
- (void)configureSettingsButtonInFrame:(NSRect)rootFrame {
// The settings button is configured to be the same size as the small image.
int settingsButtonOriginOffset =
message_center::kSmallImageSize + message_center::kSmallImagePadding;
NSRect settingsButtonFrame = NSMakeRect(
NSMaxX(rootFrame) - settingsButtonOriginOffset,
message_center::kSmallImagePadding, message_center::kSmallImageSize,
message_center::kSmallImageSize);
settingsButton_.reset(
[[HoverImageButton alloc] initWithFrame:settingsButtonFrame]);
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
[settingsButton_ setDefaultImage:rb.GetNativeImageNamed(
IDR_NOTIFICATION_SETTINGS_BUTTON_ICON)
.ToNSImage()];
[settingsButton_
setHoverImage:rb.GetNativeImageNamed(
IDR_NOTIFICATION_SETTINGS_BUTTON_ICON_HOVER)
.ToNSImage()];
[settingsButton_
setPressedImage:rb.GetNativeImageNamed(
IDR_NOTIFICATION_SETTINGS_BUTTON_ICON_PRESSED)
.ToNSImage()];
[[settingsButton_ cell] setHighlightsBy:NSOnState];
[settingsButton_ setTrackingEnabled:YES];
[settingsButton_ setBordered:NO];
[settingsButton_ setAutoresizingMask:NSViewMinYMargin];
[settingsButton_ setTarget:self];
[settingsButton_ setAction:@selector(settingsClicked:)];
[[settingsButton_ cell]
accessibilitySetOverrideValue:
l10n_util::GetNSString(
IDS_MESSAGE_NOTIFICATION_SETTINGS_BUTTON_ACCESSIBLE_NAME)
forAttribute:NSAccessibilityTitleAttribute];
}
- (NSView*)createSmallImageInFrame:(NSRect)rootFrame {
int smallImageXOffset =
message_center::kSmallImagePadding + message_center::kSmallImageSize;
NSRect boxFrame =
NSMakeRect(NSMaxX(rootFrame) - smallImageXOffset,
NSMinY(rootFrame) + message_center::kSmallImagePadding,
message_center::kSmallImageSize,
message_center::kSmallImageSize);
// Put the smallImage inside another box which can hide it from accessibility
// until we have some alt text to go with it. Once we have alt text, remove
// the box, and set NSAccessibilityDescriptionAttribute with it.
base::scoped_nsobject<NSBox> imageBox(
[[AccessibilityIgnoredBox alloc] initWithFrame:boxFrame]);
[self configureCustomBox:imageBox];
[imageBox setAutoresizingMask:NSViewMinYMargin];
NSRect smallImageFrame =
NSMakeRect(0,0,
message_center::kSmallImageSize,
message_center::kSmallImageSize);
smallImage_.reset([[NSImageView alloc] initWithFrame:smallImageFrame]);
[smallImage_ setImageScaling:NSImageScaleProportionallyUpOrDown];
[imageBox setContentView:smallImage_];
return imageBox.autorelease();
}
- (void)configureTitleInFrame:(NSRect)contentFrame {
contentFrame.size.height = 0;
title_.reset([self newLabelWithFrame:contentFrame]);
[title_ setAutoresizingMask:NSViewMinYMargin];
[title_ setTextColor:skia::SkColorToCalibratedNSColor(
message_center::kRegularTextColor)];
[title_ setFont:[NSFont messageFontOfSize:message_center::kTitleFontSize]];
}
- (void)configureBodyInFrame:(NSRect)contentFrame {
contentFrame.size.height = 0;
message_.reset([self newLabelWithFrame:contentFrame]);
[message_ setAutoresizingMask:NSViewMinYMargin];
[message_ setTextColor:skia::SkColorToCalibratedNSColor(
message_center::kRegularTextColor)];
[message_ setFont:
[NSFont messageFontOfSize:message_center::kMessageFontSize]];
}
- (void)configureContextMessageInFrame:(NSRect)contentFrame {
contentFrame.size.height = 0;
contextMessage_.reset([self newLabelWithFrame:contentFrame]);
[contextMessage_ setAutoresizingMask:NSViewMinYMargin];
[contextMessage_ setTextColor:skia::SkColorToCalibratedNSColor(
message_center::kDimTextColor)];
[contextMessage_ setFont:
[NSFont messageFontOfSize:message_center::kMessageFontSize]];
}
- (NSTextView*)newLabelWithFrame:(NSRect)frame {
NSTextView* label = [[NSTextView alloc] initWithFrame:frame];
// The labels MUST draw their background so that subpixel antialiasing can
// happen on the text.
[label setDrawsBackground:YES];
[label setBackgroundColor:skia::SkColorToCalibratedNSColor(
message_center::kNotificationBackgroundColor)];
[label setEditable:NO];
[label setSelectable:NO];
[label setTextContainerInset:NSMakeSize(0.0f, 0.0f)];
[[label textContainer] setLineFragmentPadding:0.0f];
return label;
}
- (NSRect)currentContentRect {
DCHECK(icon_);
DCHECK(closeButton_);
DCHECK(smallImage_);
NSRect iconFrame, contentFrame;
NSDivideRect([[self view] bounds], &iconFrame, &contentFrame,
NSWidth([icon_ frame]) + message_center::kIconToTextPadding,
NSMinXEdge);
// The content area is between the icon on the left and the control area
// on the right.
int controlAreaWidth =
std::max(NSWidth([closeButton_ frame]), NSWidth([smallImage_ frame]));
contentFrame.size.width -=
2 * message_center::kSmallImagePadding + controlAreaWidth;
return contentFrame;
}
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)nsfont
maxNumberOfLines:(size_t)lines
actualLines:(size_t*)actualLines {
*actualLines = 0;
if (text.empty() || lines == 0)
return base::string16();
gfx::FontList font_list((gfx::Font(nsfont)));
int width = NSWidth([self currentContentRect]);
int height = (lines + 1) * font_list.GetHeight();
std::vector<base::string16> wrapped;
gfx::ElideRectangleTextForNativeUi(text, font_list, width, height,
gfx::WRAP_LONG_WORDS, &wrapped);
// This could be possible when the input text contains only spaces.
if (wrapped.empty())
return base::string16();
if (wrapped.size() > lines) {
// Add an ellipsis to the last line. If this ellipsis makes the last line
// too wide, that line will be further elided by the gfx::ElideText below.
base::string16 last =
wrapped[lines - 1] + base::UTF8ToUTF16(gfx::kEllipsis);
if (gfx::GetStringWidth(last, font_list, gfx::Typesetter::NATIVE) > width) {
last = gfx::ElideText(last, font_list, width, gfx::ELIDE_TAIL,
gfx::Typesetter::NATIVE);
}
wrapped.resize(lines - 1);
wrapped.push_back(last);
}
*actualLines = wrapped.size();
return lines == 1 ? wrapped[0]
: base::JoinString(wrapped, base::ASCIIToUTF16("\n"));
}
- (base::string16)wrapText:(const base::string16&)text
forFont:(NSFont*)nsfont
maxNumberOfLines:(size_t)lines {
size_t unused;
return [self wrapText:text
forFont:nsfont
maxNumberOfLines:lines
actualLines:&unused];
}
@end
// Copyright (c) 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.
#import "ui/message_center/cocoa/notification_controller.h"
#include <memory>
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/macros.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "third_party/skia/include/core/SkBitmap.h"
#import "ui/base/cocoa/hover_image_button.h"
#import "ui/base/test/cocoa_helper.h"
#include "ui/message_center/fake_message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
using base::ASCIIToUTF16;
using base::UTF8ToUTF16;
namespace {
class MockMessageCenter : public message_center::FakeMessageCenter {
public:
MockMessageCenter()
: last_removed_by_user_(false),
remove_count_(0),
last_clicked_index_(-1) {}
void RemoveNotification(const std::string& id, bool by_user) override {
last_removed_id_ = id;
last_removed_by_user_ = by_user;
++remove_count_;
}
void ClickOnNotificationButton(const std::string& id,
int button_index) override {
last_clicked_id_ = id;
last_clicked_index_ = button_index;
}
const std::string& last_removed_id() const { return last_removed_id_; }
bool last_removed_by_user() const { return last_removed_by_user_; }
int remove_count() const { return remove_count_; }
const std::string& last_clicked_id() const { return last_clicked_id_; }
int last_clicked_index() const { return last_clicked_index_; }
private:
std::string last_removed_id_;
bool last_removed_by_user_;
int remove_count_;
std::string last_clicked_id_;
int last_clicked_index_;
DISALLOW_COPY_AND_ASSIGN(MockMessageCenter);
};
} // namespace
@implementation MCNotificationController (TestingInterface)
- (NSButton*)closeButton {
return closeButton_.get();
}
- (NSImageView*)smallImageView {
return smallImage_.get();
}
- (NSButton*)secondButton {
// The buttons are in Cocoa-y-order, so the 2nd button is first.
NSView* view = [[bottomView_ subviews] objectAtIndex:0];
return base::mac::ObjCCastStrict<NSButton>(view);
}
- (NSArray*)bottomSubviews {
return [bottomView_ subviews];
}
- (NSImageView*)iconView {
return icon_.get();
}
- (NSTextView*)titleView {
return title_.get();
}
- (NSTextView*)messageView {
return message_.get();
}
- (NSTextView*)contextMessageView {
return contextMessage_.get();
}
- (HoverImageButton*)settingsButton {
return settingsButton_.get();
}
- (NSView*)listView {
return listView_.get();
}
@end
namespace message_center {
class NotificationControllerTest : public ui::CocoaTest {
public:
NSImage* TestIcon() {
return [NSImage imageNamed:NSImageNameUser];
}
protected:
message_center::NotifierId DummyNotifierId() {
return message_center::NotifierId();
}
};
TEST_F(NotificationControllerTest, BasicLayout) {
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "",
ASCIIToUTF16("Added to circles"),
ASCIIToUTF16("Jonathan and 5 others"), gfx::Image(), base::string16(),
GURL(), DummyNotifierId(), message_center::RichNotificationData(),
NULL));
gfx::Image testIcon(TestIcon());
notification->set_icon(testIcon);
notification->set_small_image(testIcon);
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:NULL]);
[controller view];
EXPECT_EQ(TestIcon(), [[controller iconView] image]);
EXPECT_EQ(TestIcon(), [[controller smallImageView] image]);
EXPECT_EQ(base::SysNSStringToUTF16([[controller titleView] string]),
notification->title());
EXPECT_EQ(base::SysNSStringToUTF16([[controller messageView] string]),
notification->message());
EXPECT_EQ(controller.get(), [[controller closeButton] target]);
}
TEST_F(NotificationControllerTest, NotificationSetttingsButtonLayout) {
message_center::RichNotificationData data;
data.settings_button_handler = SettingsButtonHandler::INLINE;
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "",
ASCIIToUTF16("Added to circles"),
ASCIIToUTF16("Jonathan and 5 others"), gfx::Image(), base::string16(),
GURL("https://plus.com"), DummyNotifierId(), data, NULL));
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:nullptr]);
[controller view];
EXPECT_EQ(controller.get(), [[controller settingsButton] target]);
}
TEST_F(NotificationControllerTest, ContextMessageAsDomainNotificationLayout) {
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "",
ASCIIToUTF16("Added to circles"),
ASCIIToUTF16("Jonathan and 5 others"), gfx::Image(), base::string16(),
GURL("https://plus.com"), DummyNotifierId(),
message_center::RichNotificationData(), new NotificationDelegate()));
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:nullptr]);
[controller view];
EXPECT_EQ(base::SysNSStringToUTF8([[controller contextMessageView] string]),
"plus.com");
}
TEST_F(NotificationControllerTest, OverflowText) {
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "",
ASCIIToUTF16("This is a much longer title that should wrap "
"multiple lines."),
ASCIIToUTF16("And even the message is long. This sure is a wordy "
"notification. Are you really going to read this "
"entire thing?"),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:NULL]);
[controller view];
EXPECT_GT(NSHeight([[controller view] frame]),
message_center::kNotificationIconSize);
}
TEST_F(NotificationControllerTest, Close) {
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "an_id", base::string16(),
base::string16(), gfx::Image(), base::string16(), GURL(),
DummyNotifierId(), message_center::RichNotificationData(), NULL));
MockMessageCenter message_center;
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:&message_center]);
[controller view];
[[controller closeButton] performClick:nil];
EXPECT_EQ(1, message_center.remove_count());
EXPECT_EQ("an_id", message_center.last_removed_id());
EXPECT_TRUE(message_center.last_removed_by_user());
}
TEST_F(NotificationControllerTest, Update) {
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "",
ASCIIToUTF16("A simple title"),
ASCIIToUTF16("This message isn't too long and should fit in the"
"default bounds."),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:NULL]);
// Set up the default layout.
[controller view];
EXPECT_EQ(NSHeight([[controller view] frame]),
message_center::kNotificationIconSize);
EXPECT_FALSE([[controller iconView] image]);
EXPECT_FALSE([[controller smallImageView] image]);
// Update the icon.
gfx::Image testIcon(TestIcon());
notification->set_icon(testIcon);
notification->set_small_image(testIcon);
[controller updateNotification:notification.get()];
EXPECT_EQ(TestIcon(), [[controller iconView] image]);
EXPECT_EQ(TestIcon(), [[controller smallImageView] image]);
EXPECT_EQ(NSHeight([[controller view] frame]),
message_center::kNotificationIconSize);
}
TEST_F(NotificationControllerTest, Buttons) {
message_center::RichNotificationData optional;
message_center::ButtonInfo button1(UTF8ToUTF16("button1"));
optional.buttons.push_back(button1);
message_center::ButtonInfo button2(UTF8ToUTF16("button2"));
optional.buttons.push_back(button2);
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_BASE_FORMAT, "an_id",
base::string16(), base::string16(), gfx::Image(), base::string16(),
GURL(), DummyNotifierId(), optional, NULL));
MockMessageCenter message_center;
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:&message_center]);
[controller view];
[[controller secondButton] performClick:nil];
EXPECT_EQ("an_id", message_center.last_clicked_id());
EXPECT_EQ(1, message_center.last_clicked_index());
}
TEST_F(NotificationControllerTest, Image) {
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_BASE_FORMAT, "an_id",
base::string16(), base::string16(), gfx::Image(), base::string16(),
GURL(), DummyNotifierId(), message_center::RichNotificationData(),
NULL));
NSImage* image = [NSImage imageNamed:NSImageNameFolder];
notification->set_image(gfx::Image(image));
MockMessageCenter message_center;
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:&message_center]);
[controller view];
ASSERT_EQ(1u, [[controller bottomSubviews] count]);
ASSERT_TRUE([[[[controller bottomSubviews] lastObject] contentView]
isKindOfClass:[NSImageView class]]);
EXPECT_EQ(image,
[[[[controller bottomSubviews] lastObject] contentView] image]);
}
TEST_F(NotificationControllerTest, List) {
message_center::RichNotificationData optional;
message_center::NotificationItem item1{UTF8ToUTF16("First title"),
UTF8ToUTF16("first message")};
optional.items.push_back(item1);
message_center::NotificationItem item2{
UTF8ToUTF16("Second title"),
UTF8ToUTF16("second slightly longer message")};
optional.items.push_back(item2);
message_center::NotificationItem item3{
UTF8ToUTF16(""), // Test for empty string.
UTF8ToUTF16(" ")}; // Test for string containing only spaces.
optional.items.push_back(item3);
optional.context_message = UTF8ToUTF16("Context Message");
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_BASE_FORMAT, "an_id",
UTF8ToUTF16("Notification Title"),
UTF8ToUTF16("Notification Message - should be hidden"), gfx::Image(),
base::string16(), GURL(), DummyNotifierId(), optional, NULL));
MockMessageCenter message_center;
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:&message_center]);
[controller view];
EXPECT_FALSE([[controller titleView] isHidden]);
EXPECT_TRUE([[controller messageView] isHidden]);
EXPECT_FALSE([[controller contextMessageView] isHidden]);
EXPECT_EQ(3u, [[[controller listView] subviews] count]);
EXPECT_LT(NSMaxY([[controller listView] frame]),
NSMinY([[controller titleView] frame]));
}
TEST_F(NotificationControllerTest, NoMessage) {
message_center::RichNotificationData optional;
optional.context_message = UTF8ToUTF16("Context Message");
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_BASE_FORMAT, "an_id",
UTF8ToUTF16("Notification Title"), UTF8ToUTF16(""), gfx::Image(),
base::string16(), GURL(), DummyNotifierId(), optional, NULL));
MockMessageCenter message_center;
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:&message_center]);
[controller view];
EXPECT_FALSE([[controller titleView] isHidden]);
EXPECT_TRUE([[controller messageView] isHidden]);
EXPECT_FALSE([[controller contextMessageView] isHidden]);
}
TEST_F(NotificationControllerTest, MessageSize) {
message_center::RichNotificationData data;
std::string id("id");
NotifierId notifier_id(NotifierType::APPLICATION, "notifier");
std::unique_ptr<Notification> notification(new Notification(
NOTIFICATION_TYPE_BASE_FORMAT, id, base::UTF8ToUTF16(""),
ASCIIToUTF16("And\neven\nthe\nmessage is long.\nThis sure is wordy"),
gfx::Image(), base::string16() /* display_source */, GURL(), notifier_id,
data, NULL /* delegate */));
base::scoped_nsobject<MCNotificationController> controller(
[[MCNotificationController alloc] initWithNotification:notification.get()
messageCenter:NULL]);
// Set up the default layout.
[controller view];
auto compute_message_lines = ^{
NSString* string = [[[controller messageView] textStorage] string];
unsigned numberOfLines, index, stringLength = [string length];
for (index = 0, numberOfLines = 0; index < stringLength; numberOfLines++)
index = NSMaxRange([string lineRangeForRange:NSMakeRange(index, 0)]);
return numberOfLines;
};
// Message and no title: 5 lines.
EXPECT_EQ(5u, compute_message_lines());
// Message and one line title: 5 lines.
notification->set_title(ASCIIToUTF16("one line"));
[controller updateNotification:notification.get()];
EXPECT_EQ(5u, compute_message_lines());
// Message and two line title: 3 lines.
notification->set_title(ASCIIToUTF16("two\nlines"));
[controller updateNotification:notification.get()];
EXPECT_EQ(3u, compute_message_lines());
// Message, image and no title: 2 lines.
SkBitmap bitmap;
bitmap.allocN32Pixels(2, 2);
bitmap.eraseColor(SK_ColorGREEN);
notification->set_title(ASCIIToUTF16(""));
notification->set_image(gfx::Image::CreateFrom1xBitmap(bitmap));
[controller updateNotification:notification.get()];
EXPECT_EQ(2u, compute_message_lines());
// Message, image and one line title: 2 lines.
notification->set_title(ASCIIToUTF16("one line"));
[controller updateNotification:notification.get()];
EXPECT_EQ(2u, compute_message_lines());
// Message, image and two line title: 1 lines.
notification->set_title(ASCIIToUTF16("two\nlines"));
[controller updateNotification:notification.get()];
EXPECT_EQ(1u, compute_message_lines());
// Same as above, but context message takes away from message lines.
notification->set_context_message(UTF8ToUTF16("foo"));
notification->set_title(ASCIIToUTF16(""));
[controller updateNotification:notification.get()];
EXPECT_EQ(1u, compute_message_lines());
notification->set_title(ASCIIToUTF16("one line"));
[controller updateNotification:notification.get()];
EXPECT_EQ(1u, compute_message_lines());
notification->set_title(ASCIIToUTF16("two\nlines"));
[controller updateNotification:notification.get()];
EXPECT_EQ(0u, compute_message_lines());
}
} // namespace message_center
// Copyright 2014 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.
#ifndef UI_MESSAGE_CENTER_COCOA_OPAQUE_VIEWS_H_
#define UI_MESSAGE_CENTER_COCOA_OPAQUE_VIEWS_H_
#import <Cocoa/Cocoa.h>
#include "base/mac/scoped_nsobject.h"
// MCDropDown is the same as an NSPopupButton except that it fills its
// background with a settable color.
@interface MCDropDown : NSPopUpButton {
@private
base::scoped_nsobject<NSColor> backgroundColor_;
}
// Gets and sets the bubble's background color.
- (NSColor*)backgroundColor;
- (void)setBackgroundColor:(NSColor*)backgroundColor;
@end
// MCTextField fills its background with an opaque color. It also configures
// the view to have a plan appearance, without bezel, border, editing, etc.
@interface MCTextField : NSTextField {
@private
base::scoped_nsobject<NSColor> backgroundColor_;
}
// Use this method to create the text field. The color is required so it
// can correctly subpixel antialias.
- (id)initWithFrame:(NSRect)frameRect backgroundColor:(NSColor*)color;
@end
#endif // UI_MESSAGE_CENTER_COCOA_OPAQUE_VIEWS_H_
// Copyright 2014 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.
#import "ui/message_center/cocoa/opaque_views.h"
@implementation MCDropDown
// The view must be opaque to render subpixel antialiasing.
- (BOOL)isOpaque {
return YES;
}
// The view must also fill its background to render subpixel antialiasing.
- (void)drawRect:(NSRect)dirtyRect {
[backgroundColor_ set];
NSRectFill(dirtyRect);
[super drawRect:dirtyRect];
}
- (NSColor*)backgroundColor {
return backgroundColor_;
}
- (void)setBackgroundColor:(NSColor*)backgroundColor {
backgroundColor_.reset([backgroundColor retain]);
}
@end
@implementation MCTextField
- (id)initWithFrame:(NSRect)frameRect backgroundColor:(NSColor*)color {
self = [self initWithFrame:frameRect];
if (self) {
[self setBackgroundColor:color];
backgroundColor_.reset([color retain]);
}
return self;
}
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
[self setAutoresizingMask:NSViewMinYMargin];
[self setBezeled:NO];
[self setBordered:NO];
[self setEditable:NO];
[self setSelectable:NO];
[self setDrawsBackground:YES];
}
return self;
}
// The view must be opaque to render subpixel antialiasing.
- (BOOL)isOpaque {
return YES;
}
// The view must also fill its background to render subpixel antialiasing.
- (void)drawRect:(NSRect)dirtyRect {
[backgroundColor_ set];
NSRectFill(dirtyRect);
[super drawRect:dirtyRect];
}
@end
// Copyright (c) 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.
#ifndef UI_MESSAGE_CENTER_COCOA_POPUP_COLLECTION_H_
#define UI_MESSAGE_CENTER_COCOA_POPUP_COLLECTION_H_
#import <Cocoa/Cocoa.h>
#include <memory>
#include <set>
#include "base/mac/scoped_block.h"
#import "base/mac/scoped_nsobject.h"
#include "ui/message_center/message_center_export.h"
namespace message_center {
class MessageCenter;
class MessageCenterObserver;
}
namespace message_center {
typedef void(^AnimationEndedCallback)();
}
// A popup collection interfaces with the MessageCenter as an observer. It will
// arrange notifications on the screen as popups, starting in the upper-right
// corner, going to the bottom of the screen. This class maintains ownership of
// the Cocoa controllers and windows of the notifications.
MESSAGE_CENTER_EXPORT
@interface MCPopupCollection : NSObject {
@private
// The message center that is responsible for the notifications. Weak, global.
message_center::MessageCenter* messageCenter_;
// MessageCenterObserver implementation.
std::unique_ptr<message_center::MessageCenterObserver> observer_;
// Array of all on-screen popup notifications.
base::scoped_nsobject<NSMutableArray> popups_;
// Array of all on-screen popup notifications that are being faded out
// for removal.
base::scoped_nsobject<NSMutableArray> popupsBeingRemoved_;
// For testing only. If not a zero rect, this is the screen size to use
// for laying out popups.
NSRect testingScreenFrame_;
// The duration of the popup animation, in the number of seconds.
NSTimeInterval popupAnimationDuration_;
// Set of notification IDs for those popups to be updated when all existing
// animations end.
std::set<std::string> pendingUpdateNotificationIDs_;
// Set of notification IDs for those popups to be closed when all existing
// animations end.
std::set<std::string> pendingRemoveNotificationIDs_;
// Set of notification IDs for those popups that are being animated due to
// showing, bounds change or closing.
std::set<std::string> animatingNotificationIDs_;
// For testing only. If set, the callback will be called when the animation
// ends.
base::mac::ScopedBlock<message_center::AnimationEndedCallback>
testingAnimationEndedCallback_;
}
// Designated initializer that construct an instance to observe |messageCenter|.
- (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter;
// Returns true if an animation is being played.
- (BOOL)isAnimating;
// Returns the duration of the popup animation.
- (NSTimeInterval)popupAnimationDuration;
// Called when the animation of a popup ends.
- (void)onPopupAnimationEnded:(const std::string&)notificationID;
@end
@interface MCPopupCollection (ExposedForTesting)
- (NSArray*)popups;
// Setter for the testingScreenFrame_.
- (void)setScreenFrame:(NSRect)frame;
// Setter for changing the animation duration. The testing code could set it
// to a very small value to expedite the test running.
- (void)setAnimationDuration:(NSTimeInterval)duration;
// Setter for testingAnimationEndedCallback_. The testing code could set it
// to get called back when the animation ends.
- (void)setAnimationEndedCallback:
(message_center::AnimationEndedCallback)callback;
@end
#endif // UI_MESSAGE_CENTER_COCOA_POPUP_COLLECTION_H_
// Copyright (c) 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.
#import "ui/message_center/cocoa/popup_collection.h"
#import "ui/message_center/cocoa/notification_controller.h"
#import "ui/message_center/cocoa/popup_controller.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_observer.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
const float kAnimationDuration = 0.2;
@interface MCPopupCollection (Private)
// Returns the primary screen's visible frame rectangle.
- (NSRect)screenFrame;
// Shows a popup, if there is room on-screen, for the given notification.
// Returns YES if the notification was actually displayed.
- (BOOL)addNotification:(const message_center::Notification*)notification;
// Updates the contents of the notification with the given ID.
- (void)updateNotification:(const std::string&)notificationID;
// Removes a popup from the screen and lays out new notifications that can
// now potentially fit on the screen.
- (void)removeNotification:(const std::string&)notificationID;
// Closes all the popups.
- (void)removeAllNotifications;
// Returns the index of the popup showing the notification with the given ID.
- (NSUInteger)indexOfPopupWithNotificationID:(const std::string&)notificationID;
// Repositions all popup notifications if needed.
- (void)layoutNotifications;
// Fits as many new notifications as possible on screen.
- (void)layoutNewNotifications;
// Process notifications pending to remove when no animation is being played.
- (void)processPendingRemoveNotifications;
// Process notifications pending to update when no animation is being played.
- (void)processPendingUpdateNotifications;
@end
namespace {
class PopupCollectionObserver : public message_center::MessageCenterObserver {
public:
PopupCollectionObserver(message_center::MessageCenter* message_center,
MCPopupCollection* popup_collection)
: message_center_(message_center),
popup_collection_(popup_collection) {
message_center_->AddObserver(this);
}
~PopupCollectionObserver() override { message_center_->RemoveObserver(this); }
void OnNotificationAdded(const std::string& notification_id) override {
[popup_collection_ layoutNewNotifications];
}
void OnNotificationRemoved(const std::string& notification_id,
bool user_id) override {
[popup_collection_ removeNotification:notification_id];
}
void OnNotificationUpdated(const std::string& notification_id) override {
[popup_collection_ updateNotification:notification_id];
}
void OnBlockingStateChanged(
message_center::NotificationBlocker* blocker) override {
[popup_collection_ layoutNewNotifications];
}
private:
message_center::MessageCenter* message_center_; // Weak, global.
MCPopupCollection* popup_collection_; // Weak, owns this.
};
} // namespace
@implementation MCPopupCollection
- (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter {
if ((self = [super init])) {
messageCenter_ = messageCenter;
observer_.reset(new PopupCollectionObserver(messageCenter_, self));
popups_.reset([[NSMutableArray alloc] init]);
popupsBeingRemoved_.reset([[NSMutableArray alloc] init]);
popupAnimationDuration_ = kAnimationDuration;
}
return self;
}
- (void)dealloc {
[popupsBeingRemoved_ makeObjectsPerformSelector:
@selector(markPopupCollectionGone)];
[self removeAllNotifications];
[super dealloc];
}
- (BOOL)isAnimating {
return !animatingNotificationIDs_.empty();
}
- (NSTimeInterval)popupAnimationDuration {
return popupAnimationDuration_;
}
- (void)onPopupAnimationEnded:(const std::string&)notificationID {
NSUInteger index = [popupsBeingRemoved_ indexOfObjectPassingTest:
^BOOL(id popup, NSUInteger index, BOOL* stop) {
return [popup notificationID] == notificationID;
}];
if (index != NSNotFound)
[popupsBeingRemoved_ removeObjectAtIndex:index];
animatingNotificationIDs_.erase(notificationID);
if (![self isAnimating])
[self layoutNotifications];
// Give the testing code a chance to do something, i.e. quitting the test
// run loop.
if (![self isAnimating] && testingAnimationEndedCallback_)
testingAnimationEndedCallback_.get()();
}
// Testing API /////////////////////////////////////////////////////////////////
- (NSArray*)popups {
return popups_.get();
}
- (void)setScreenFrame:(NSRect)frame {
testingScreenFrame_ = frame;
}
- (void)setAnimationDuration:(NSTimeInterval)duration {
popupAnimationDuration_ = duration;
}
- (void)setAnimationEndedCallback:
(message_center::AnimationEndedCallback)callback {
testingAnimationEndedCallback_.reset(Block_copy(callback));
}
// Private /////////////////////////////////////////////////////////////////////
- (NSRect)screenFrame {
if (!NSIsEmptyRect(testingScreenFrame_))
return testingScreenFrame_;
return [[[NSScreen screens] firstObject] visibleFrame];
}
- (BOOL)addNotification:(const message_center::Notification*)notification {
// Wait till all existing animations end.
if ([self isAnimating])
return NO;
// The popup is owned by itself. It will be released at close.
MCPopupController* popup =
[[MCPopupController alloc] initWithNotification:notification
messageCenter:messageCenter_
popupCollection:self];
NSRect screenFrame = [self screenFrame];
NSRect popupFrame = [popup bounds];
CGFloat x = NSMaxX(screenFrame) - message_center::kMarginBetweenPopups -
NSWidth(popupFrame);
CGFloat y = 0;
MCPopupController* bottomPopup = [popups_ lastObject];
if (!bottomPopup) {
y = NSMaxY(screenFrame);
} else {
y = NSMinY([bottomPopup bounds]);
}
y -= message_center::kMarginBetweenPopups + NSHeight(popupFrame);
if (y > NSMinY(screenFrame)) {
animatingNotificationIDs_.insert(notification->id());
NSRect bounds = [popup bounds];
bounds.origin.x = x;
bounds.origin.y = y;
[popup showWithAnimation:bounds];
[popups_ addObject:popup];
messageCenter_->DisplayedNotification(
notification->id(), message_center::DISPLAY_SOURCE_POPUP);
return YES;
}
// The popup cannot fit on screen, so it has to be closed now.
[popup close];
return NO;
}
- (void)updateNotification:(const std::string&)notificationID {
// The notification may not be on screen. Create it if needed.
if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound) {
[self layoutNewNotifications];
return;
}
// Don't bother with the update if the notification is going to be removed.
if (pendingRemoveNotificationIDs_.find(notificationID) !=
pendingRemoveNotificationIDs_.end()) {
return;
}
pendingUpdateNotificationIDs_.insert(notificationID);
[self processPendingUpdateNotifications];
}
- (void)removeNotification:(const std::string&)notificationID {
// The notification may not be on screen.
if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
return;
// Don't bother with the update if the notification is going to be removed.
pendingUpdateNotificationIDs_.erase(notificationID);
pendingRemoveNotificationIDs_.insert(notificationID);
[self processPendingRemoveNotifications];
}
- (void)removeAllNotifications {
// In rare cases, the popup collection would be gone while an animation is
// still playing. For exmaple, the test code could show a new notification
// and dispose the collection immediately. Close the popup without animation
// when this is the case.
if ([self isAnimating])
[popups_ makeObjectsPerformSelector:@selector(close)];
else
[popups_ makeObjectsPerformSelector:@selector(closeWithAnimation)];
[popups_ makeObjectsPerformSelector:@selector(markPopupCollectionGone)];
[popups_ removeAllObjects];
}
- (NSUInteger)indexOfPopupWithNotificationID:
(const std::string&)notificationID {
return [popups_ indexOfObjectPassingTest:
^BOOL(id popup, NSUInteger index, BOOL* stop) {
return [popup notificationID] == notificationID;
}];
}
- (void)layoutNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
NSRect screenFrame = [self screenFrame];
// The popup starts at top-right corner.
CGFloat maxY = NSMaxY(screenFrame);
// Iterate all notifications and reposition each if needed. If one does not
// fit on screen, close it and any other on-screen popups that come after it.
NSUInteger removeAt = NSNotFound;
for (NSUInteger i = 0; i < [popups_ count]; ++i) {
MCPopupController* popup = [popups_ objectAtIndex:i];
NSRect oldFrame = [popup bounds];
NSRect frame = oldFrame;
frame.origin.y =
maxY - message_center::kMarginBetweenPopups - NSHeight(frame);
// If this popup does not fit on screen, stop repositioning and close this
// and subsequent popups.
if (NSMinY(frame) < NSMinY(screenFrame)) {
removeAt = i;
break;
}
if (!NSEqualRects(frame, oldFrame)) {
[popup setBounds:frame];
animatingNotificationIDs_.insert([popup notificationID]);
}
// Set the new maximum Y to be the bottom of this notification.
maxY = NSMinY(frame);
}
if (removeAt != NSNotFound) {
// Remove any popups that are on screen but no longer fit.
while ([popups_ count] >= removeAt && [popups_ count]) {
[[popups_ lastObject] close];
[popups_ removeLastObject];
}
} else {
[self layoutNewNotifications];
}
[self processPendingRemoveNotifications];
[self processPendingUpdateNotifications];
}
- (void)layoutNewNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
// Display any new popups that can now fit on screen, starting from the
// oldest notification that has not been shown up.
const auto& allPopups = messageCenter_->GetPopupNotifications();
for (auto it = allPopups.rbegin(); it != allPopups.rend(); ++it) {
if ([self indexOfPopupWithNotificationID:(*it)->id()] == NSNotFound) {
// If there's no room left on screen to display notifications, stop
// trying.
if (![self addNotification:*it])
break;
}
}
}
- (void)processPendingRemoveNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
for (const auto& notificationID : pendingRemoveNotificationIDs_) {
NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
if (index != NSNotFound) {
[[popups_ objectAtIndex:index] closeWithAnimation];
animatingNotificationIDs_.insert(notificationID);
// Still need to track popup object and only remove it after the animation
// ends. We need to notify these objects that the collection is gone
// in the collection destructor.
[popupsBeingRemoved_ addObject:[popups_ objectAtIndex:index]];
[popups_ removeObjectAtIndex:index];
}
}
pendingRemoveNotificationIDs_.clear();
}
- (void)processPendingUpdateNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
if (pendingUpdateNotificationIDs_.empty())
return;
// Go through all model objects in the message center. If there is a replaced
// notification, the controller's current model object may be stale.
const auto& modelPopups = messageCenter_->GetPopupNotifications();
for (auto iter = modelPopups.begin(); iter != modelPopups.end(); ++iter) {
const std::string& notificationID = (*iter)->id();
// Does the notification need to be updated?
std::set<std::string>::iterator pendingUpdateIter =
pendingUpdateNotificationIDs_.find(notificationID);
if (pendingUpdateIter == pendingUpdateNotificationIDs_.end())
continue;
pendingUpdateNotificationIDs_.erase(pendingUpdateIter);
// Is the notification still on screen?
NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
if (index == NSNotFound)
continue;
MCPopupController* popup = [popups_ objectAtIndex:index];
CGFloat oldHeight =
NSHeight([[[popup notificationController] view] frame]);
CGFloat newHeight = NSHeight(
[[popup notificationController] updateNotification:*iter]);
// The notification has changed height. This requires updating the popup
// window.
if (oldHeight != newHeight) {
NSRect popupFrame = [popup bounds];
popupFrame.origin.y -= newHeight - oldHeight;
popupFrame.size.height += newHeight - oldHeight;
[popup setBounds:popupFrame];
animatingNotificationIDs_.insert([popup notificationID]);
}
}
// Notification update could be received when a notification is excluded from
// the popup notification list but still remains in the full notification
// list, as in clicking the popup. In that case, the popup should be closed.
for (auto iter = pendingUpdateNotificationIDs_.begin();
iter != pendingUpdateNotificationIDs_.end(); ++iter) {
pendingRemoveNotificationIDs_.insert(*iter);
}
pendingUpdateNotificationIDs_.clear();
// Start re-layout of all notifications, so that it readjusts the Y origin of
// all updated popups and any popups that come below them.
[self layoutNotifications];
}
@end
// Copyright (c) 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.
#import "ui/message_center/cocoa/popup_collection.h"
#include <memory>
#include <utility>
#include "base/mac/scoped_nsobject.h"
#include "base/run_loop.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_task_environment.h"
#import "ui/base/test/cocoa_helper.h"
#import "ui/message_center/cocoa/notification_controller.h"
#import "ui/message_center/cocoa/popup_controller.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
using base::ASCIIToUTF16;
namespace message_center {
class PopupCollectionTest : public ui::CocoaTest {
public:
PopupCollectionTest()
: scoped_task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::UI) {
message_center::MessageCenter::Initialize();
center_ = message_center::MessageCenter::Get();
collection_.reset(
[[MCPopupCollection alloc] initWithMessageCenter:center_]);
[collection_ setAnimationDuration:0.001];
[collection_ setAnimationEndedCallback:^{
if (nested_run_loop_.get())
nested_run_loop_->Quit();
}];
}
void TearDown() override {
collection_.reset(); // Close all popups.
ui::CocoaTest::TearDown();
}
~PopupCollectionTest() override { message_center::MessageCenter::Shutdown(); }
message_center::NotifierId DummyNotifierId() {
return message_center::NotifierId();
}
void AddThreeNotifications() {
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
ASCIIToUTF16("This is the first notification to"
" be displayed"),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->AddNotification(std::move(notification));
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "2", ASCIIToUTF16("Two"),
ASCIIToUTF16("This is the second notification."), gfx::Image(),
base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->AddNotification(std::move(notification));
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "3", ASCIIToUTF16("Three"),
ASCIIToUTF16("This is the third notification "
"that has a much longer body "
"than the other notifications. It "
"may not fit on the screen if we "
"set the screen size too small or "
"if the notification is way too big"),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->AddNotification(std::move(notification));
WaitForAnimationEnded();
}
bool CheckSpacingBetween(MCPopupController* upper, MCPopupController* lower) {
CGFloat minY = NSMinY([[upper window] frame]);
CGFloat maxY = NSMaxY([[lower window] frame]);
CGFloat delta = minY - maxY;
EXPECT_EQ(message_center::kMarginBetweenPopups, delta);
return delta == message_center::kMarginBetweenPopups;
}
void WaitForAnimationEnded() {
if (![collection_ isAnimating])
return;
nested_run_loop_.reset(new base::RunLoop());
nested_run_loop_->Run();
nested_run_loop_.reset();
}
base::test::ScopedTaskEnvironment scoped_task_environment_;
std::unique_ptr<base::RunLoop> nested_run_loop_;
message_center::MessageCenter* center_;
base::scoped_nsobject<MCPopupCollection> collection_;
};
TEST_F(PopupCollectionTest, AddThreeCloseOne) {
EXPECT_EQ(0u, [[collection_ popups] count]);
AddThreeNotifications();
EXPECT_EQ(3u, [[collection_ popups] count]);
center_->RemoveNotification("2", true);
WaitForAnimationEnded();
EXPECT_EQ(2u, [[collection_ popups] count]);
}
TEST_F(PopupCollectionTest, AttemptFourOneOffscreen) {
[collection_ setScreenFrame:NSMakeRect(0, 0, 800, 300)];
EXPECT_EQ(0u, [[collection_ popups] count]);
AddThreeNotifications();
EXPECT_EQ(2u, [[collection_ popups] count]); // "3" does not fit on screen.
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "4", ASCIIToUTF16("Four"),
ASCIIToUTF16("This is the fourth notification."), gfx::Image(),
base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->AddNotification(std::move(notification));
WaitForAnimationEnded();
// Remove "1" and "3" should fit on screen.
center_->RemoveNotification("1", true);
WaitForAnimationEnded();
ASSERT_EQ(2u, [[collection_ popups] count]);
EXPECT_EQ("2", [[[collection_ popups] objectAtIndex:0] notificationID]);
EXPECT_EQ("3", [[[collection_ popups] objectAtIndex:1] notificationID]);
// Remove "2" and "4" should fit on screen.
center_->RemoveNotification("2", true);
WaitForAnimationEnded();
ASSERT_EQ(2u, [[collection_ popups] count]);
EXPECT_EQ("3", [[[collection_ popups] objectAtIndex:0] notificationID]);
EXPECT_EQ("4", [[[collection_ popups] objectAtIndex:1] notificationID]);
}
TEST_F(PopupCollectionTest, LayoutSpacing) {
const CGFloat kScreenSize = 500;
[collection_ setScreenFrame:NSMakeRect(0, 0, kScreenSize, kScreenSize)];
AddThreeNotifications();
NSArray* popups = [collection_ popups];
EXPECT_EQ(message_center::kMarginBetweenPopups,
kScreenSize - NSMaxY([[[popups objectAtIndex:0] window] frame]));
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
[popups objectAtIndex:1]));
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
[popups objectAtIndex:2]));
// Set priority so that kMaxVisiblePopupNotifications does not hide it.
message_center::RichNotificationData optional;
optional.priority = message_center::HIGH_PRIORITY;
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "4", ASCIIToUTF16("Four"),
ASCIIToUTF16("This is the fourth notification."), gfx::Image(),
base::string16(), GURL(), DummyNotifierId(), optional, NULL));
center_->AddNotification(std::move(notification));
WaitForAnimationEnded();
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:2],
[popups objectAtIndex:3]));
// Remove "2".
center_->RemoveNotification("2", true);
WaitForAnimationEnded();
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
[popups objectAtIndex:1]));
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
[popups objectAtIndex:2]));
// Remove "1".
center_->RemoveNotification("2", true);
WaitForAnimationEnded();
EXPECT_EQ(message_center::kMarginBetweenPopups,
kScreenSize - NSMaxY([[[popups objectAtIndex:0] window] frame]));
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
[popups objectAtIndex:1]));
}
TEST_F(PopupCollectionTest, TinyScreen) {
[collection_ setScreenFrame:NSMakeRect(0, 0, 800, 100)];
EXPECT_EQ(0u, [[collection_ popups] count]);
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
ASCIIToUTF16("This is the first notification to"
" be displayed"),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->AddNotification(std::move(notification));
WaitForAnimationEnded();
EXPECT_EQ(1u, [[collection_ popups] count]);
// Now give the notification a longer message so that it no longer fits.
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
ASCIIToUTF16("This is now a very very very very "
"very very very very very very very "
"very very very very very very very "
"very very very very very very very "
"very very very very very very very "
"very very very very very very very "
"very very very very very very very "
"long notification."),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->UpdateNotification("1", std::move(notification));
WaitForAnimationEnded();
EXPECT_EQ(0u, [[collection_ popups] count]);
}
TEST_F(PopupCollectionTest, UpdateIconAndBody) {
AddThreeNotifications();
NSArray* popups = [collection_ popups];
EXPECT_EQ(3u, [popups count]);
// Update "2" icon.
MCNotificationController* controller =
[[popups objectAtIndex:1] notificationController];
EXPECT_FALSE([[controller iconView] image]);
center_->SetNotificationIcon(
"2", gfx::Image([NSImage imageNamed:NSImageNameUser]));
WaitForAnimationEnded();
EXPECT_TRUE([[controller iconView] image]);
EXPECT_EQ(3u, [popups count]);
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
[popups objectAtIndex:1]));
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
[popups objectAtIndex:2]));
// Replace "1".
controller = [[popups objectAtIndex:0] notificationController];
NSRect old_frame = [[controller view] frame];
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1",
ASCIIToUTF16("One is going to get a much longer "
"title than it previously had."),
ASCIIToUTF16("This is the first notification to "
"be displayed, but it will also be "
"updated to have a significantly "
"longer body"),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->AddNotification(std::move(notification));
WaitForAnimationEnded();
EXPECT_GT(NSHeight([[controller view] frame]), NSHeight(old_frame));
// Test updated spacing.
EXPECT_EQ(3u, [popups count]);
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:0],
[popups objectAtIndex:1]));
EXPECT_TRUE(CheckSpacingBetween([popups objectAtIndex:1],
[popups objectAtIndex:2]));
EXPECT_EQ("1", [[popups objectAtIndex:0] notificationID]);
EXPECT_EQ("2", [[popups objectAtIndex:1] notificationID]);
EXPECT_EQ("3", [[popups objectAtIndex:2] notificationID]);
}
TEST_F(PopupCollectionTest, UpdatePriority) {
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
ASCIIToUTF16("This notification should not yet toast."), gfx::Image(),
base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
notification->set_priority(-1);
center_->AddNotification(std::move(notification));
WaitForAnimationEnded();
NSArray* popups = [collection_ popups];
EXPECT_EQ(0u, [popups count]);
// Raise priority -1 to 1. Notification should display.
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
ASCIIToUTF16("This notification should now toast"), gfx::Image(),
base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
notification->set_priority(1);
center_->UpdateNotification("1", std::move(notification));
WaitForAnimationEnded();
EXPECT_EQ(1u, [popups count]);
}
TEST_F(PopupCollectionTest, CloseCollectionBeforeNewPopupAnimationEnds) {
// Add a notification and don't wait for the animation to finish.
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
ASCIIToUTF16("This is the first notification to"
" be displayed"),
gfx::Image(), base::string16(), GURL(), DummyNotifierId(),
message_center::RichNotificationData(), NULL));
center_->AddNotification(std::move(notification));
// Release the popup collection before the animation ends. No crash should
// be expected.
collection_.reset();
}
TEST_F(PopupCollectionTest, CloseCollectionBeforeClosePopupAnimationEnds) {
AddThreeNotifications();
// Remove a notification and don't wait for the animation to finish.
center_->RemoveNotification("1", true);
// Release the popup collection before the animation ends. No crash should
// be expected.
collection_.reset();
}
TEST_F(PopupCollectionTest, CloseCollectionBeforeUpdatePopupAnimationEnds) {
AddThreeNotifications();
// Update a notification and don't wait for the animation to finish.
std::unique_ptr<message_center::Notification> notification;
notification.reset(new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "1", ASCIIToUTF16("One"),
ASCIIToUTF16("New message."), gfx::Image(), base::string16(), GURL(),
DummyNotifierId(), message_center::RichNotificationData(), NULL));
center_->UpdateNotification("1", std::move(notification));
// Release the popup collection before the animation ends. No crash should
// be expected.
collection_.reset();
}
} // namespace message_center
// Copyright (c) 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.
#ifndef UI_MESSAGE_CENTER_COCOA_POPUP_CONTROLLER_H_
#define UI_MESSAGE_CENTER_COCOA_POPUP_CONTROLLER_H_
#import <Cocoa/Cocoa.h>
#include <string>
#import "base/mac/scoped_nsobject.h"
#import "ui/base/cocoa/tracking_area.h"
#include "ui/message_center/message_center_export.h"
namespace message_center {
class MessageCenter;
class Notification;
}
@class MCNotificationController;
@class MCPopupCollection;
// A window controller that hosts a notification as a popup balloon on the
// user's desktop. The window controller manages its lifetime because the
// popup collection will be destructed when the last popup is closed.
MESSAGE_CENTER_EXPORT
@interface MCPopupController : NSWindowController<NSAnimationDelegate> {
@private
// Global message center. Weak.
message_center::MessageCenter* messageCenter_;
// The collection that contains the popup. Weak.
MCPopupCollection* popupCollection_;
// The view controller that provide's the popup content view.
base::scoped_nsobject<MCNotificationController> notificationController_;
// If the swipe-away gesture received NSEventPhaseEnded.
BOOL swipeGestureEnded_;
// The frame of the popup before any swipe animation started. Used to
// calculate the animating position of the window when swiping away.
NSRect originalFrame_;
// Is the popup currently being closed?
BOOL isClosing_;
#ifndef NDEBUG
// Has the popup been closed before being dealloc-ed.
BOOL hasBeenClosed_;
#endif
// The current bounds of the popup frame if no animation is playing.
// Otherwise, it is the target bounds of the popup frame.
NSRect bounds_;
// Used to play animation when the popup shows, changes bounds and closes.
base::scoped_nsobject<NSViewAnimation> boundsAnimation_;
// Used to track the popup for mouse entered and exited events.
ui::ScopedCrTrackingArea trackingArea_;
}
// Designated initializer.
- (id)initWithNotification:(const message_center::Notification*)notification
messageCenter:(message_center::MessageCenter*)messageCenter
popupCollection:(MCPopupCollection*)popupCollection;
// Accessor for the view controller.
- (MCNotificationController*)notificationController;
// Accessor for the notification model object.
- (const message_center::Notification*)notification;
// Gets the notification ID. This string is owned by the NotificationController
// rather than the model object, so it's safe to use after the Notification has
// been deleted.
- (const std::string&)notificationID;
// Shows the window with the sliding animation.
- (void)showWithAnimation:(NSRect)newBounds;
// Closes the window with the fade-out animation.
- (void)closeWithAnimation;
// Tells that the popupCollection_ is gone.
- (void)markPopupCollectionGone;
// Returns the window bounds. This is the target bounds to go to if the bounds
// animation is playing.
- (NSRect)bounds;
// Changes the window bounds with animation.
- (void)setBounds:(NSRect)newBounds;
@end
#endif // UI_MESSAGE_CENTER_COCOA_POPUP_CONTROLLER_H_
// Copyright (c) 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.
#import "ui/message_center/cocoa/popup_controller.h"
#include <cmath>
#import "base/mac/foundation_util.h"
#import "base/mac/sdk_forward_declarations.h"
#import "ui/base/cocoa/window_size_constants.h"
#import "ui/message_center/cocoa/notification_controller.h"
#import "ui/message_center/cocoa/popup_collection.h"
#include "ui/message_center/message_center.h"
////////////////////////////////////////////////////////////////////////////////
@interface MCPopupController (Private)
- (void)notificationSwipeStarted;
- (void)notificationSwipeMoved:(CGFloat)amount;
- (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete;
// This setter for |boundsAnimation_| also cleans up the state of the previous
// |boundsAnimation_|.
- (void)setBoundsAnimation:(NSViewAnimation*)animation;
// Constructs an NSViewAnimation from |dictionary|, which should be a view
// animation dictionary.
- (NSViewAnimation*)animationWithDictionary:(NSDictionary*)dictionary;
@end
// Window Subclass /////////////////////////////////////////////////////////////
@interface MCPopupWindow : NSPanel {
// The cumulative X and Y scrollingDeltas since the -scrollWheel: event began.
NSPoint totalScrollDelta_;
}
@end
@implementation MCPopupWindow
- (void)scrollWheel:(NSEvent*)event {
// Gesture swiping only exists on 10.7+.
if (![event respondsToSelector:@selector(phase)])
return;
NSEventPhase phase = [event phase];
BOOL shouldTrackSwipe = NO;
if (phase == NSEventPhaseBegan) {
totalScrollDelta_ = NSZeroPoint;
} else if (phase == NSEventPhaseChanged) {
shouldTrackSwipe = YES;
totalScrollDelta_.x += [event scrollingDeltaX];
totalScrollDelta_.y += [event scrollingDeltaY];
}
// Only allow horizontal scrolling.
if (std::abs(totalScrollDelta_.x) < std::abs(totalScrollDelta_.y))
return;
if (shouldTrackSwipe) {
MCPopupController* controller =
base::mac::ObjCCastStrict<MCPopupController>([self windowController]);
BOOL directionInverted = [event isDirectionInvertedFromDevice];
auto handler = ^(CGFloat gestureAmount, NSEventPhase phase,
BOOL isComplete, BOOL* stop) {
// The swipe direction should match the direction the user's fingers
// are moving, not the interpreted scroll direction.
if (directionInverted)
gestureAmount *= -1;
if (phase == NSEventPhaseBegan) {
[controller notificationSwipeStarted];
return;
}
[controller notificationSwipeMoved:gestureAmount];
BOOL ended = phase == NSEventPhaseEnded;
if (ended || isComplete)
[controller notificationSwipeEnded:ended complete:isComplete];
};
[event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection
dampenAmountThresholdMin:-1
max:1
usingHandler:handler];
}
}
@end
////////////////////////////////////////////////////////////////////////////////
@implementation MCPopupController
- (id)initWithNotification:(const message_center::Notification*)notification
messageCenter:(message_center::MessageCenter*)messageCenter
popupCollection:(MCPopupCollection*)popupCollection {
base::scoped_nsobject<MCPopupWindow> window([[MCPopupWindow alloc]
initWithContentRect:ui::kWindowSizeDeterminedLater
styleMask:NSNonactivatingPanelMask
backing:NSBackingStoreBuffered
defer:NO]);
if ((self = [super initWithWindow:window])) {
messageCenter_ = messageCenter;
popupCollection_ = popupCollection;
notificationController_.reset(
[[MCNotificationController alloc] initWithNotification:notification
messageCenter:messageCenter_]);
bounds_ = [[notificationController_ view] frame];
[window setFloatingPanel:YES];
[window setBecomesKeyOnlyIfNeeded:YES];
[window
setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces |
NSWindowCollectionBehaviorFullScreenAuxiliary];
[window setHasShadow:YES];
[window setContentView:[notificationController_ view]];
trackingArea_.reset(
[[CrTrackingArea alloc] initWithRect:NSZeroRect
options:NSTrackingInVisibleRect |
NSTrackingMouseEnteredAndExited |
NSTrackingActiveAlways
owner:self
userInfo:nil]);
[[window contentView] addTrackingArea:trackingArea_.get()];
}
return self;
}
#ifndef NDEBUG
- (void)dealloc {
DCHECK(hasBeenClosed_);
[super dealloc];
}
#endif
- (void)close {
#ifndef NDEBUG
hasBeenClosed_ = YES;
#endif
[self setBoundsAnimation:nil];
if (trackingArea_.get())
[[[self window] contentView] removeTrackingArea:trackingArea_.get()];
[super close];
[self performSelectorOnMainThread:@selector(release)
withObject:nil
waitUntilDone:NO
modes:@[ NSDefaultRunLoopMode ]];
}
- (MCNotificationController*)notificationController {
return notificationController_.get();
}
- (const message_center::Notification*)notification {
return [notificationController_ notification];
}
- (const std::string&)notificationID {
return [notificationController_ notificationID];
}
// Private /////////////////////////////////////////////////////////////////////
- (void)notificationSwipeStarted {
originalFrame_ = [[self window] frame];
swipeGestureEnded_ = NO;
}
- (void)notificationSwipeMoved:(CGFloat)amount {
NSWindow* window = [self window];
[window setAlphaValue:1.0 - std::abs(amount)];
NSRect frame = [window frame];
CGFloat originalMin = NSMinX(originalFrame_);
frame.origin.x = originalMin + (NSMidX(originalFrame_) - originalMin) *
-amount;
[window setFrame:frame display:YES];
}
- (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete {
swipeGestureEnded_ |= ended;
if (swipeGestureEnded_ && isComplete) {
messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
[popupCollection_ onPopupAnimationEnded:[self notificationID]];
}
}
- (void)setBoundsAnimation:(NSViewAnimation*)animation {
[boundsAnimation_ stopAnimation];
[boundsAnimation_ setDelegate:nil];
boundsAnimation_.reset([animation retain]);
}
- (NSViewAnimation*)animationWithDictionary:(NSDictionary*)dictionary {
return [[[NSViewAnimation alloc]
initWithViewAnimations:@[ dictionary ]] autorelease];
}
- (void)animationDidEnd:(NSAnimation*)animation {
DCHECK_EQ(animation, boundsAnimation_.get());
[self setBoundsAnimation:nil];
[popupCollection_ onPopupAnimationEnded:[self notificationID]];
if (isClosing_)
[self close];
}
- (void)showWithAnimation:(NSRect)newBounds {
bounds_ = newBounds;
NSRect startBounds = newBounds;
startBounds.origin.x += startBounds.size.width;
[[self window] setFrame:startBounds display:NO];
[[self window] setAlphaValue:0];
[[self window] setCanHide:NO];
[self showWindow:nil];
// Slide-in and fade-in simultaneously.
NSDictionary* animationDict = @{
NSViewAnimationTargetKey : [self window],
NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds],
NSViewAnimationEffectKey : NSViewAnimationFadeInEffect
};
NSViewAnimation* animation = [self animationWithDictionary:animationDict];
[self setBoundsAnimation:animation];
[boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
[boundsAnimation_ setDelegate:self];
[boundsAnimation_ startAnimation];
}
- (void)closeWithAnimation {
if (isClosing_)
return;
#ifndef NDEBUG
hasBeenClosed_ = YES;
#endif
isClosing_ = YES;
// If the notification was swiped closed, do not animate it as the
// notification has already faded out.
if (swipeGestureEnded_) {
[self close];
return;
}
NSDictionary* animationDict = @{
NSViewAnimationTargetKey : [self window],
NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
};
NSViewAnimation* animation = [self animationWithDictionary:animationDict];
[self setBoundsAnimation:animation];
[boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
[boundsAnimation_ setDelegate:self];
[boundsAnimation_ startAnimation];
}
- (void)markPopupCollectionGone {
popupCollection_ = nil;
}
- (NSRect)bounds {
return bounds_;
}
- (void)setBounds:(NSRect)newBounds {
if (isClosing_ || NSEqualRects(bounds_ , newBounds))
return;
bounds_ = newBounds;
NSDictionary* animationDict = @{
NSViewAnimationTargetKey : [self window],
NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds]
};
NSViewAnimation* animation = [self animationWithDictionary:animationDict];
[self setBoundsAnimation:animation];
[boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
[boundsAnimation_ setDelegate:self];
[boundsAnimation_ startAnimation];
}
- (void)mouseEntered:(NSEvent*)event {
messageCenter_->PausePopupTimers();
}
- (void)mouseExited:(NSEvent*)event {
messageCenter_->RestartPopupTimers();
}
@end
// Copyright (c) 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.
#import "ui/message_center/cocoa/popup_controller.h"
#include <memory>
#include "base/mac/scoped_nsobject.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "ui/base/test/cocoa_helper.h"
#include "ui/message_center/public/cpp/notification.h"
using base::ASCIIToUTF16;
namespace message_center {
class PopupControllerTest : public ui::CocoaTest {
};
TEST_F(PopupControllerTest, Creation) {
std::unique_ptr<message_center::Notification> notification(
new message_center::Notification(
message_center::NOTIFICATION_TYPE_SIMPLE, "",
ASCIIToUTF16("Added to circles"),
ASCIIToUTF16("Jonathan and 5 others"), gfx::Image(), base::string16(),
GURL(), message_center::NotifierId(),
message_center::RichNotificationData(), NULL));
base::scoped_nsobject<MCPopupController> controller(
[[MCPopupController alloc] initWithNotification:notification.get()
messageCenter:nil
popupCollection:nil]);
// Add an extra ref count for scoped_nsobject since MCPopupController will
// release itself when it is being closed.
[controller retain];
EXPECT_TRUE([controller window]);
EXPECT_EQ(notification.get(), [controller notification]);
[controller showWindow:nil];
[controller close];
}
} // namespace message_center
...@@ -16,10 +16,6 @@ ...@@ -16,10 +16,6 @@
#include "ui/base/ui_base_paths.h" #include "ui/base/ui_base_paths.h"
#include "ui/gl/test/gl_surface_test_support.h" #include "ui/gl/test/gl_surface_test_support.h"
#if defined(OS_MACOSX)
#include "base/test/mock_chrome_application_mac.h"
#endif
namespace { namespace {
class MessageCenterTestSuite : public base::TestSuite { class MessageCenterTestSuite : public base::TestSuite {
...@@ -28,9 +24,6 @@ class MessageCenterTestSuite : public base::TestSuite { ...@@ -28,9 +24,6 @@ class MessageCenterTestSuite : public base::TestSuite {
protected: protected:
void Initialize() override { void Initialize() override {
#if defined(OS_MACOSX)
mock_cr_app::RegisterMockCrApp();
#endif
gl::GLSurfaceTestSupport::InitializeOneOff(); gl::GLSurfaceTestSupport::InitializeOneOff();
base::TestSuite::Initialize(); base::TestSuite::Initialize();
ui::RegisterPathProvider(); ui::RegisterPathProvider();
......
...@@ -118,21 +118,8 @@ ...@@ -118,21 +118,8 @@
<if expr="is_macosx or is_ios"> <if expr="is_macosx or is_ios">
<structure type="chrome_scaled_image" name="IDR_MENU_HIERARCHY_ARROW" file="mac/menu_hierarchy_arrow.png" /> <structure type="chrome_scaled_image" name="IDR_MENU_HIERARCHY_ARROW" file="mac/menu_hierarchy_arrow.png" />
</if> </if>
<if expr="toolkit_views or is_ios"> <if expr="toolkit_views and not is_macosx">
<if expr="is_win">
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_CLOSE" file="win/notification_close.png"/>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_CLOSE_HOVER" file="win/notification_close_hover.png"/>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_CLOSE_PRESSED" file="win/notification_close_pressed.png"/>
</if>
<if expr="not is_win">
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_CLOSE" file="common/notification_close.png"/>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_CLOSE_HOVER" file="common/notification_close_hover.png"/>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_CLOSE_PRESSED" file="common/notification_close_pressed.png"/>
</if>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_SETTINGS" file="common/notification_settings.png"/> <structure type="chrome_scaled_image" name="IDR_NOTIFICATION_SETTINGS" file="common/notification_settings.png"/>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_SETTINGS_BUTTON_ICON" file="common/notification_settings_button.png"/>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_SETTINGS_BUTTON_ICON_HOVER" file="common/notification_settings_button_hover.png"/>
<structure type="chrome_scaled_image" name="IDR_NOTIFICATION_SETTINGS_BUTTON_ICON_PRESSED" file="common/notification_settings_button_pressed.png"/>
</if> </if>
<if expr="not is_android and not is_ios"> <if expr="not is_android and not is_ios">
<structure type="chrome_scaled_image" name="IDR_NTP_DEFAULT_FAVICON" file="common/ntp_default_favicon.png" /> <structure type="chrome_scaled_image" name="IDR_NTP_DEFAULT_FAVICON" file="common/ntp_default_favicon.png" />
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment