1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/cocoa/bookmarks/bookmark_drag_drop_cocoa.h"
6
7#import <Cocoa/Cocoa.h>
8
9#include <cmath>
10
11#include "base/logging.h"
12#include "base/mac/scoped_nsobject.h"
13#include "base/message_loop/message_loop.h"
14#include "base/strings/string16.h"
15#include "base/strings/sys_string_conversions.h"
16#include "chrome/browser/bookmarks/bookmark_model.h"
17#include "chrome/browser/bookmarks/bookmark_model_factory.h"
18#include "chrome/browser/bookmarks/bookmark_node_data.h"
19#include "chrome/browser/profiles/profile.h"
20#include "chrome/browser/ui/bookmarks/bookmark_drag_drop.h"
21#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h"
22#include "grit/ui_resources.h"
23#include "ui/base/resource/resource_bundle.h"
24#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
25
26namespace chrome {
27
28namespace {
29
30// Make a drag image from the drop data.
31NSImage* MakeDragImage(BookmarkModel* model,
32                       const std::vector<const BookmarkNode*>& nodes) {
33  if (nodes.size() == 1) {
34    const BookmarkNode* node = nodes[0];
35    const gfx::Image& favicon = model->GetFavicon(node);
36    return DragImageForBookmark(
37        favicon.IsEmpty() ? nil : favicon.ToNSImage(), node->GetTitle());
38  } else {
39    // TODO(feldstein): Do something better than this. Should have badging
40    // and a single drag image.
41    // http://crbug.com/37264
42    return [NSImage imageNamed:NSImageNameMultipleDocuments];
43  }
44}
45
46// Draws string |title| within box |frame|, positioning it at the origin.
47// Truncates text with fading if it is too long to fit horizontally.
48// Based on code from GradientButtonCell but simplified where possible.
49void DrawTruncatedTitle(NSAttributedString* title, NSRect frame) {
50  NSSize size = [title size];
51  if (std::floor(size.width) <= NSWidth(frame)) {
52    [title drawAtPoint:frame.origin];
53    return;
54  }
55
56  // Gradient is about twice our line height long.
57  CGFloat gradient_width = std::min(size.height * 2, NSWidth(frame) / 4);
58  NSRect solid_part, gradient_part;
59  NSDivideRect(frame, &gradient_part, &solid_part, gradient_width, NSMaxXEdge);
60  CGContextRef context = static_cast<CGContextRef>(
61      [[NSGraphicsContext currentContext] graphicsPort]);
62  CGContextBeginTransparencyLayerWithRect(context, NSRectToCGRect(frame), 0);
63  { // Draw text clipped to frame.
64    gfx::ScopedNSGraphicsContextSaveGState scoped_state;
65    [NSBezierPath clipRect:frame];
66    [title drawAtPoint:frame.origin];
67  }
68
69  NSColor* color = [NSColor blackColor];
70  NSColor* alpha_color = [color colorWithAlphaComponent:0.0];
71  base::scoped_nsobject<NSGradient> mask(
72      [[NSGradient alloc] initWithStartingColor:color endingColor:alpha_color]);
73  // Draw the gradient mask.
74  CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
75  [mask drawFromPoint:NSMakePoint(NSMaxX(frame) - gradient_width,
76                                  NSMinY(frame))
77              toPoint:NSMakePoint(NSMaxX(frame),
78                                  NSMinY(frame))
79              options:NSGradientDrawsBeforeStartingLocation];
80  CGContextEndTransparencyLayer(context);
81}
82
83}  // namespace
84
85NSImage* DragImageForBookmark(NSImage* favicon, const base::string16& title) {
86  // If no favicon, use a default.
87  if (!favicon) {
88    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
89    favicon = rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).ToNSImage();
90  }
91
92  // If no title, just use icon.
93  if (title.empty())
94    return favicon;
95  NSString* ns_title = base::SysUTF16ToNSString(title);
96
97  // Set the look of the title.
98  NSDictionary* attrs =
99      [NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:
100                                           [NSFont smallSystemFontSize]]
101                                  forKey:NSFontAttributeName];
102  base::scoped_nsobject<NSAttributedString> rich_title(
103      [[NSAttributedString alloc] initWithString:ns_title attributes:attrs]);
104
105  // Set up sizes and locations for rendering.
106  const CGFloat kIconMargin = 2.0;  // Gap between icon and text.
107  CGFloat text_left = [favicon size].width + kIconMargin;
108  NSSize drag_image_size = [favicon size];
109  NSSize text_size = [rich_title size];
110  CGFloat max_text_width = bookmarks::kDefaultBookmarkWidth - text_left;
111  text_size.width = std::min(text_size.width, max_text_width);
112  drag_image_size.width = text_left + text_size.width;
113
114  // Render the drag image.
115  NSImage* drag_image =
116      [[[NSImage alloc] initWithSize:drag_image_size] autorelease];
117  [drag_image lockFocus];
118  [favicon drawAtPoint:NSZeroPoint
119              fromRect:NSZeroRect
120             operation:NSCompositeSourceOver
121              fraction:0.7];
122  NSRect target_text_rect = NSMakeRect(text_left, 0,
123                                       text_size.width, drag_image_size.height);
124  DrawTruncatedTitle(rich_title, target_text_rect);
125  [drag_image unlockFocus];
126
127  return drag_image;
128}
129
130void DragBookmarks(Profile* profile,
131                   const std::vector<const BookmarkNode*>& nodes,
132                   gfx::NativeView view) {
133  DCHECK(!nodes.empty());
134
135  // Allow nested message loop so we get DnD events as we drag this around.
136  bool was_nested = base::MessageLoop::current()->IsNested();
137  base::MessageLoop::current()->SetNestableTasksAllowed(true);
138
139  BookmarkNodeData drag_data(nodes);
140  drag_data.SetOriginatingProfile(profile);
141  drag_data.WriteToClipboard(ui::CLIPBOARD_TYPE_DRAG);
142
143  // Synthesize an event for dragging, since we can't be sure that
144  // [NSApp currentEvent] will return a valid dragging event.
145  NSWindow* window = [view window];
146  NSPoint position = [window mouseLocationOutsideOfEventStream];
147  NSTimeInterval event_time = [[NSApp currentEvent] timestamp];
148  NSEvent* drag_event = [NSEvent mouseEventWithType:NSLeftMouseDragged
149                                           location:position
150                                      modifierFlags:NSLeftMouseDraggedMask
151                                          timestamp:event_time
152                                       windowNumber:[window windowNumber]
153                                            context:nil
154                                        eventNumber:0
155                                         clickCount:1
156                                           pressure:1.0];
157
158  // TODO(avi): Do better than this offset.
159  NSImage* drag_image = chrome::MakeDragImage(
160      BookmarkModelFactory::GetForProfile(profile), nodes);
161  NSSize image_size = [drag_image size];
162  position.x -= std::floor(image_size.width / 2);
163  position.y -= std::floor(image_size.height / 5);
164  [window dragImage:drag_image
165                 at:position
166             offset:NSZeroSize
167              event:drag_event
168         pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard]
169             source:nil
170          slideBack:YES];
171
172  base::MessageLoop::current()->SetNestableTasksAllowed(was_nested);
173}
174
175}  // namespace chrome
176