blob: 128fa871a1200fed4812a4dc243fa438d95373a1 [file] [log] [blame]
license.botbf09a502008-08-24 00:55:551// Copyright (c) 2006-2008 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.
initial.commit09911bf2008-07-26 23:55:294
5#include "chrome/browser/history_view.h"
6
7#include "base/string_util.h"
[email protected]5cca3a52008-08-19 22:35:298#include "base/time_format.h"
initial.commit09911bf2008-07-26 23:55:299#include "base/word_iterator.h"
10#include "chrome/browser/browsing_data_remover.h"
11#include "chrome/browser/drag_utils.h"
[email protected]cd1adc22009-01-16 01:29:2212#include "chrome/browser/metrics/user_metrics.h"
[email protected]f3ec7742009-01-15 00:59:1613#include "chrome/browser/tab_contents/native_ui_contents.h"
14#include "chrome/browser/tab_contents/page_navigator.h"
initial.commit09911bf2008-07-26 23:55:2915#include "chrome/browser/views/bookmark_bubble_view.h"
16#include "chrome/browser/views/event_utils.h"
17#include "chrome/browser/views/star_toggle.h"
[email protected]1eb89e82008-08-15 12:27:0318#include "chrome/common/drag_drop_types.h"
initial.commit09911bf2008-07-26 23:55:2919#include "chrome/common/gfx/chrome_canvas.h"
20#include "chrome/common/gfx/favicon_size.h"
[email protected]a4feef82008-10-02 15:11:2221#include "chrome/common/l10n_util.h"
initial.commit09911bf2008-07-26 23:55:2922#include "chrome/common/resource_bundle.h"
23#include "chrome/common/time_format.h"
24#include "chrome/common/win_util.h"
25#include "chrome/views/link.h"
[email protected]a0dde122008-11-21 20:51:2026#include "chrome/views/widget.h"
initial.commit09911bf2008-07-26 23:55:2927
28#include "generated_resources.h"
29
[email protected]e1acf6f2008-10-27 20:43:3330using base::Time;
31using base::TimeDelta;
32
initial.commit09911bf2008-07-26 23:55:2933// The extra-wide space between groups of entries for each new day.
34static const int kDayHeadingHeight = 50;
35
36// The space between groups of entries within a day.
37static const int kSessionBreakHeight = 24;
38
39// Amount of time between page-views that triggers a break (in microseconds).
40static const int64 kSessionBreakTime = 1800 * 1000000; // 30 minutes
41
42// Horizontal space between the left edge of the entries and the
43// left edge of the view.
44static const int kLeftMargin = 38;
45
46// x-position of the page title (massage this so it visually matches
47// kDestinationSearchOffset in native_ui_contents.cc
48static const int kPageTitleOffset = 102;
49
50// x-position of the Time
51static const int kTimeOffset = 24;
52
53// Vertical offset for the delete control (distance from the top of a day
54// break segment).
55static const int kDeleteControlOffset = 30;
56
57// x-position of the session gap filler (currently a thin vertical line
58// joining the times on either side of a session gap).
59static const int kSessionGapOffset = 16;
60
61// Horizontal space between the right edge of the item
62// and the right edge of the view.
63static const int kRightMargin = 20;
64
65// The ideal height of an entry. This may change depending on font line-height.
66static const int kSearchResultsHeight = 72;
67static const int kBrowseResultsHeight = 24;
68
69// How much room to leave above the first result.
70static const int kResultsMargin = 24;
71
72// Height of the results text area.
73static const int kResultTextHeight = 24;
74
75// Height of the area when there are no results to display.
76static const int kNoResultTextHeight = 48;
77static const int kNoResultMinWidth = 512;
78
79// Extra vertical space between the different lines of text.
80// (Note that the height() variables are baseline-to-baseline already.)
81static const int kLeading = 2;
82
83// The amount of space from the edges of an entry to the edges of its contents.
84static const int kEntryPadding = 8;
85
86// Padding between the icons (star, favicon) and other elements.
87static const int kIconPadding = 4;
88
89// SnippetRenderer is a View that can displayed text with bolding and wrapping.
90// It's used to display search result snippets.
[email protected]c2dacc92008-10-16 23:51:3891class SnippetRenderer : public views::View {
initial.commit09911bf2008-07-26 23:55:2992 public:
93 SnippetRenderer();
94
95 // Set the text snippet.
96 void SetSnippet(const Snippet& snippet);
97
98 int GetLineHeight();
99
100 virtual void Paint(ChromeCanvas* canvas);
101
102 private:
103 // The snippet that we're drawing.
104 Snippet snippet_;
105
106 // Font for plain text.
107 ChromeFont text_font_;
108 // Font for match text. (TODO(evanm): use red for Chinese (bug 844518).)
109 ChromeFont match_font_;
110
111 // Layout/draw a substring of the snippet from [start,end) at (x, y).
112 // ProcessRun is strictly for text in a single line: it doesn't do any
113 // word-wrapping, and is used as a helper for laying out multiple lines
114 // of output in Pain().
115 // match_iter is an iterator in match_runs_ that covers a region
116 // before or at start.
117 // When canvas is NULL, does no drawing and only computes the size.
118 // Returns the pixel width of the run.
119 // TODO(evanm): this could be optimizing by only measuring the text once
120 // and returning the layout, but it's worth profiling first.
121 int ProcessRun(ChromeCanvas* canvas,
122 int x,
123 int y,
124 Snippet::MatchPositions::const_iterator match_iter,
[email protected]c29962f22008-12-03 00:47:58125 size_t start,
126 size_t end);
initial.commit09911bf2008-07-26 23:55:29127
128 DISALLOW_EVIL_CONSTRUCTORS(SnippetRenderer);
129};
130
131SnippetRenderer::SnippetRenderer() {
132 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
133
134 text_font_ = resource_bundle.GetFont(ResourceBundle::WebFont);
135 match_font_ = text_font_.DeriveFont(0, ChromeFont::BOLD);
136}
137
138void SnippetRenderer::SetSnippet(const Snippet& snippet) {
139 snippet_ = snippet;
140}
141
142int SnippetRenderer::GetLineHeight() {
143 return std::max(text_font_.height(), match_font_.height()) + kLeading;
144}
145
146void SnippetRenderer::Paint(ChromeCanvas* canvas) {
147 const int line_height = GetLineHeight();
148
149 WordIterator iter(snippet_.text(), WordIterator::BREAK_LINE);
150 if (!iter.Init())
151 return;
152 Snippet::MatchPositions::const_iterator match_iter =
153 snippet_.matches().begin();
154
155 int x = 0;
156 int y = 0;
157 while (iter.Advance()) {
158 // Advance match_iter to a run that potentially covers this region.
159 while (match_iter != snippet_.matches().end() &&
160 match_iter->second <= iter.prev()) {
161 ++match_iter;
162 }
163
164 // The region from iter.prev() to iter.pos() should be on one line.
165 // It can be a mixture of bold and non-bold, so first lay it out to
166 // compute its width.
167 const int width = ProcessRun(NULL, 0, 0,
168 match_iter, iter.prev(), iter.pos());
169 // Advance to the next line if necessary.
[email protected]6f3bb6c2008-09-17 22:25:33170 if (x + width > View::width()) {
initial.commit09911bf2008-07-26 23:55:29171 x = 0;
172 y += line_height;
[email protected]6f3bb6c2008-09-17 22:25:33173 if (y >= height())
initial.commit09911bf2008-07-26 23:55:29174 return; // Out of vertical space.
175 }
176 ProcessRun(canvas, x, y, match_iter, iter.prev(), iter.pos());
177 x += width;
178 }
179}
180
181int SnippetRenderer::ProcessRun(
[email protected]c29962f22008-12-03 00:47:58182 ChromeCanvas* canvas,
183 int x,
184 int y,
initial.commit09911bf2008-07-26 23:55:29185 Snippet::MatchPositions::const_iterator match_iter,
[email protected]c29962f22008-12-03 00:47:58186 size_t start,
187 size_t end) {
initial.commit09911bf2008-07-26 23:55:29188 int total_width = 0;
189
190 while (start < end) {
191 // Advance match_iter to the next match that can cover the current
192 // position.
193 while (match_iter != snippet_.matches().end() &&
194 match_iter->second <= start) {
195 ++match_iter;
196 }
197
198 // Determine the next substring to process by examining whether
199 // we're before a match or within a match.
200 ChromeFont* font = &text_font_;
[email protected]c29962f22008-12-03 00:47:58201 size_t next = end;
initial.commit09911bf2008-07-26 23:55:29202 if (match_iter != snippet_.matches().end()) {
203 if (match_iter->first > start) {
204 // We're in a plain region.
205 next = std::min(match_iter->first, end);
206 } else if (match_iter->first <= start &&
207 match_iter->second > start) {
208 // We're in a match region.
209 font = &match_font_;
210 next = std::min(match_iter->second, end);
211 }
212 }
213
214 // Draw/layout the text.
215 const std::wstring run = snippet_.text().substr(start, next - start);
216 const int width = font->GetStringWidth(run);
217 if (canvas) {
218 canvas->DrawStringInt(run, *font, SkColorSetRGB(0, 0, 0),
219 x + total_width, y,
[email protected]6f3bb6c2008-09-17 22:25:33220 width, height(),
initial.commit09911bf2008-07-26 23:55:29221 ChromeCanvas::TEXT_VALIGN_BOTTOM);
222 }
223
224 // Advance.
225 total_width += width;
226 start = next;
227 }
228
229 return total_width;
230}
231
232// A View for an individual history result.
[email protected]c2dacc92008-10-16 23:51:38233class HistoryItemRenderer : public views::View,
234 public views::LinkController,
initial.commit09911bf2008-07-26 23:55:29235 public StarToggle::Delegate {
236 public:
237 HistoryItemRenderer(HistoryView* parent, bool show_full);
238 ~HistoryItemRenderer();
239
240 // Set the BaseHistoryModel that this renderer displays.
241 // model_index is the index of this entry, and is passed to all of the
242 // model functions.
243 void SetModel(BaseHistoryModel* model, int model_index);
244
245 // Set whether we should display full size or partial-sized items.
246 void SetDisplayStyle(bool show_full);
247
248 // Layout the contents of this view.
249 void Layout();
250
251 protected:
252 // Overridden to do a drag if over the favicon or thumbnail.
253 virtual int GetDragOperations(int press_x, int press_y);
254 virtual void WriteDragData(int press_x, int press_y, OSExchangeData* data);
255
256 private:
257 // Regions drags may originate from.
258 enum DragRegion {
259 FAV_ICON,
260 THUMBNAIL,
261 NONE
262 };
263
264 // The thickness of the border drawn around thumbnails.
265 static const int kThumbnailBorderWidth = 1;
266
267 // The height of the thumbnail images.
268 static const int kThumbnailHeight = kSearchResultsHeight - kEntryPadding * 2;
269
270 // The width of the thumbnail images.
271 static const int kThumbnailWidth = static_cast<int>(1.44 * kThumbnailHeight);
272
273 // The maximum width of a snippet - we want to constrain this to make
274 // snippets easier to read (like Google search results).
275 static const int kMaxSnippetWidth = 500;
276
277 // Returns the bounds of the thumbnail.
278 void GetThumbnailBounds(CRect* rect);
279
280 // Convert a GURL into a displayable string.
281 std::wstring DisplayURL(const GURL& url);
282
283 virtual void Paint(ChromeCanvas* canvas);
284
285 // Notification that the star was changed.
286 virtual void StarStateChanged(bool state);
287
288 // Notification that the link was clicked.
[email protected]c2dacc92008-10-16 23:51:38289 virtual void LinkActivated(views::Link* source, int event_flags);
initial.commit09911bf2008-07-26 23:55:29290
291 // Returns the region the mouse is over.
292 DragRegion GetDragRegion(int x, int y);
293
294 // The HistoryView containing this view.
295 HistoryView* parent_;
296
297 // Whether we're showing a fullsize item, or a single-line item.
298 bool show_full_;
299
300 // The model and index of this entry within the model.
301 BaseHistoryModel* model_;
302 int model_index_;
303
304 // Widgets.
305 StarToggle* star_toggle_;
[email protected]c2dacc92008-10-16 23:51:38306 views::Link* title_link_;
307 views::Label* time_label_;
initial.commit09911bf2008-07-26 23:55:29308 SnippetRenderer* snippet_label_;
309
310 DISALLOW_EVIL_CONSTRUCTORS(HistoryItemRenderer);
311};
312
313HistoryItemRenderer::HistoryItemRenderer(HistoryView* parent,
314 bool show_full)
315 : parent_(parent),
316 show_full_(show_full) {
317 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
318
319 ChromeFont text_font(resource_bundle.GetFont(ResourceBundle::WebFont));
320
321 star_toggle_ = new StarToggle(this);
322 star_toggle_->set_change_state_immediately(false);
323 AddChildView(star_toggle_);
324
[email protected]c2dacc92008-10-16 23:51:38325 title_link_ = new views::Link();
initial.commit09911bf2008-07-26 23:55:29326 title_link_->SetFont(text_font);
[email protected]c2dacc92008-10-16 23:51:38327 title_link_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
initial.commit09911bf2008-07-26 23:55:29328 title_link_->SetController(this);
329 AddChildView(title_link_);
330
331 const SkColor kTimeColor = SkColorSetRGB(136, 136, 136); // Gray.
332
[email protected]c2dacc92008-10-16 23:51:38333 time_label_ = new views::Label();
initial.commit09911bf2008-07-26 23:55:29334 ChromeFont time_font(text_font);
335 time_label_->SetFont(time_font);
336 time_label_->SetColor(kTimeColor);
[email protected]c2dacc92008-10-16 23:51:38337 time_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
initial.commit09911bf2008-07-26 23:55:29338 AddChildView(time_label_);
339
340 snippet_label_ = new SnippetRenderer();
341 AddChildView(snippet_label_);
342}
343
344HistoryItemRenderer::~HistoryItemRenderer() {
345}
346
347void HistoryItemRenderer::GetThumbnailBounds(CRect* rect) {
348 DCHECK(rect);
[email protected]6f3bb6c2008-09-17 22:25:33349 rect->right = width() - kEntryPadding;
initial.commit09911bf2008-07-26 23:55:29350 rect->left = rect->right - kThumbnailWidth;
351 rect->top = kEntryPadding;
352 rect->bottom = rect->top + kThumbnailHeight;
353}
354
355std::wstring HistoryItemRenderer::DisplayURL(const GURL& url) {
356 std::string url_str = url.spec();
357 // Hide the "http://" prefix like web search does.
358 if (url_str.find("http://") == 0)
359 url_str.erase(0, strlen("http://"));
360 return UTF8ToWide(url_str);
361}
362
363void HistoryItemRenderer::Paint(ChromeCanvas* canvas) {
[email protected]c2dacc92008-10-16 23:51:38364 views::View::Paint(canvas);
initial.commit09911bf2008-07-26 23:55:29365
366 // Draw thumbnail or placeholder.
367 if (show_full_) {
368 SkBitmap* thumbnail = model_->GetThumbnail(model_index_);
369 CRect thumbnail_rect;
370 GetThumbnailBounds(&thumbnail_rect); // Includes border
371
372 // If the UI layout is right-to-left, we must mirror the bounds so that we
373 // render the bitmap in the correct position.
374 gfx::Rect mirrored_rect(thumbnail_rect);
375 thumbnail_rect.MoveToX(MirroredLeftPointForRect(mirrored_rect));
376
377 if (thumbnail) {
378 // This will create a MipMap for the bitmap if one doesn't exist already
379 // (it's a NOP if a MipMap already exists). This will give much smoother
380 // results for the scaled-down thumbnails.
381 thumbnail->buildMipMap(false);
382
383 canvas->DrawBitmapInt(
384 *thumbnail,
385 0, 0, thumbnail->width(), thumbnail->height(),
386 thumbnail_rect.left, thumbnail_rect.top,
387 thumbnail_rect.Width(), thumbnail_rect.Height(),
388 true);
389 } else {
390 canvas->FillRectInt(SK_ColorWHITE,
391 thumbnail_rect.left, thumbnail_rect.top,
392 thumbnail_rect.Width(), thumbnail_rect.Height());
393 }
394 canvas->DrawRectInt(SkColorSetRGB(153, 153, 191),
395 thumbnail_rect.left, thumbnail_rect.top,
396 thumbnail_rect.Width(), thumbnail_rect.Height());
397 }
398
399 // Draw the favicon.
400 SkBitmap* favicon = model_->GetFavicon(model_index_);
401 if (favicon) {
402 // WARNING: if you change these values, update the code that determines
403 // whether we should allow a drag (GetDragRegion).
initial.commit09911bf2008-07-26 23:55:29404
405 // We need to tweak the favicon position if the UI layout is RTL.
406 gfx::Rect favicon_bounds;
[email protected]0d8ea702008-10-14 17:03:07407 favicon_bounds.set_x(title_link_->x() - kIconPadding - kFavIconSize);
initial.commit09911bf2008-07-26 23:55:29408 favicon_bounds.set_y(kEntryPadding);
409 favicon_bounds.set_width(favicon->width());
410 favicon_bounds.set_height(favicon->height());
411 favicon_bounds.set_x(MirroredLeftPointForRect(favicon_bounds));
412
413 // Drawing the bitmap using the possibly adjusted bounds.
414 canvas->DrawBitmapInt(*favicon, favicon_bounds.x(), favicon_bounds.y());
415 }
416
417 // The remainder of painting is handled by drawing our children, which
418 // is managed by the View class for us.
419}
420
421void HistoryItemRenderer::Layout() {
422 // Figure out the maximum x-position of any text.
423 CRect thumbnail_rect;
424 int max_x;
425 if (show_full_) {
426 GetThumbnailBounds(&thumbnail_rect);
427 max_x = thumbnail_rect.left - kEntryPadding;
428 } else {
[email protected]6f3bb6c2008-09-17 22:25:33429 max_x = width() - kEntryPadding;
initial.commit09911bf2008-07-26 23:55:29430 }
431
432 // Calculate the ideal positions of some items. If possible, we
433 // want the title to line up with kPageTitleOffset (and we would lay
434 // out the star and the favicon to the left of that), but in cases
435 // where font or language choices cause the time label to be
436 // horizontally large, we need to push everything to the right.
437 //
438 // If you fiddle with the calculations below, you may need to adjust
439 // the favicon painting in Paint() (and in GetDragRegion by extension).
440
441 // First we calculate the ideal position of the title.
442 int title_x = kPageTitleOffset;
443
444 // We calculate the size of the star.
[email protected]154f8bc2008-10-15 18:02:30445 gfx::Size star_size = star_toggle_->GetPreferredSize();
initial.commit09911bf2008-07-26 23:55:29446
447 // Measure and lay out the time label, and potentially move
448 // our title to suit.
initial.commit09911bf2008-07-26 23:55:29449 Time visit_time = model_->GetVisitTime(model_index_);
450 int time_x = kTimeOffset;
451 if (visit_time.is_null()) {
452 // We will get null times if the page has never been visited, for example,
453 // bookmarks after you clear history.
454 time_label_->SetText(std::wstring());
455 } else if (show_full_) {
456 time_x = 0;
[email protected]5cca3a52008-08-19 22:35:29457 time_label_->SetText(base::TimeFormatShortDate(visit_time));
initial.commit09911bf2008-07-26 23:55:29458 } else {
[email protected]5cca3a52008-08-19 22:35:29459 time_label_->SetText(base::TimeFormatTimeOfDay(visit_time));
initial.commit09911bf2008-07-26 23:55:29460 }
[email protected]154f8bc2008-10-15 18:02:30461 gfx::Size time_size = time_label_->GetPreferredSize();
initial.commit09911bf2008-07-26 23:55:29462
463 time_label_->SetBounds(time_x, kEntryPadding,
[email protected]154f8bc2008-10-15 18:02:30464 time_size.width(), time_size.height());
initial.commit09911bf2008-07-26 23:55:29465
466 // Calculate the position of the favicon.
467 int favicon_x = title_x - kFavIconSize - kIconPadding;
468
469 // Now we look to see if the favicon overlaps the time label,
470 // and if so, we push the title to the right. If we're not
471 // showing the time label, then ignore this step.
[email protected]154f8bc2008-10-15 18:02:30472 int overlap = favicon_x - (time_x + time_size.width() + kIconPadding);
initial.commit09911bf2008-07-26 23:55:29473 if (overlap < 0) {
474 title_x -= overlap;
475 }
476
477 // Populate and measure the title label.
478 const std::wstring& title = model_->GetTitle(model_index_);
479 if (!title.empty())
480 title_link_->SetText(title);
481 else
482 title_link_->SetText(l10n_util::GetString(IDS_HISTORY_UNTITLED_TITLE));
[email protected]154f8bc2008-10-15 18:02:30483 gfx::Size title_size = title_link_->GetPreferredSize();
initial.commit09911bf2008-07-26 23:55:29484
485 // Lay out the title label.
486 int max_title_x;
487
488 max_title_x = std::max(0, max_x - title_x);
489
[email protected]154f8bc2008-10-15 18:02:30490 if (title_size.width() + kEntryPadding > max_title_x) {
initial.commit09911bf2008-07-26 23:55:29491 // We need to shrink the title to make everything fit.
[email protected]154f8bc2008-10-15 18:02:30492 title_size.set_width(max_title_x - kEntryPadding);
initial.commit09911bf2008-07-26 23:55:29493 }
494 title_link_->SetBounds(title_x, kEntryPadding,
[email protected]154f8bc2008-10-15 18:02:30495 title_size.width(), title_size.height());
initial.commit09911bf2008-07-26 23:55:29496
497 // Lay out the star.
498 if (model_->IsStarred(model_index_)) {
[email protected]154f8bc2008-10-15 18:02:30499 star_toggle_->SetBounds(title_x + title_size.width() + kIconPadding,
500 kEntryPadding, star_size.width(),
501 star_size.height());
initial.commit09911bf2008-07-26 23:55:29502 star_toggle_->SetState(true);
503 star_toggle_->SetVisible(true);
504 } else {
505 star_toggle_->SetVisible(false);
506 }
507
508 // Lay out the snippet label.
509 snippet_label_->SetVisible(show_full_);
510 if (show_full_) {
511 const Snippet& snippet = model_->GetSnippet(model_index_);
512 if (snippet.text().empty()) {
513 snippet_label_->SetSnippet(Snippet()); // Bug 843469 will fix this.
514 } else {
515 snippet_label_->SetSnippet(snippet);
516 }
517 snippet_label_->SetBounds(title_x,
518 kEntryPadding + snippet_label_->GetLineHeight(),
519 std::min(
520 static_cast<int>(thumbnail_rect.left -
521 title_x),
522 kMaxSnippetWidth) -
523 kEntryPadding * 2,
524 snippet_label_->GetLineHeight() * 2);
525 }
526}
527
528int HistoryItemRenderer::GetDragOperations(int x, int y) {
529 if (GetDragRegion(x, y) != NONE)
530 return DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_LINK;
531 return DragDropTypes::DRAG_NONE;
532}
533
534void HistoryItemRenderer::WriteDragData(int press_x,
535 int press_y,
536 OSExchangeData* data) {
537 DCHECK(GetDragOperations(press_x, press_y) != DragDropTypes::DRAG_NONE);
538
539 if (GetDragRegion(press_x, press_y) == FAV_ICON)
540 UserMetrics::RecordAction(L"History_DragIcon", model_->profile());
541 else
542 UserMetrics::RecordAction(L"History_DragThumbnail", model_->profile());
543
544 SkBitmap icon;
545 if (model_->GetFavicon(model_index_))
546 icon = *model_->GetFavicon(model_index_);
547
548 drag_utils::SetURLAndDragImage(model_->GetURL(model_index_),
549 model_->GetTitle(model_index_),
550 icon, data);
551}
552
553void HistoryItemRenderer::SetModel(BaseHistoryModel* model, int model_index) {
554 DCHECK(model_index < model->GetItemCount());
555 model_ = model;
556 model_index_ = model_index;
557}
558
559void HistoryItemRenderer::SetDisplayStyle(bool show_full) {
560 show_full_ = show_full;
561}
562
563void HistoryItemRenderer::StarStateChanged(bool state) {
564 // Show the user a tip that can be used to edit the bookmark/star.
[email protected]96b667d2008-10-14 20:58:44565 gfx::Point star_location;
[email protected]c2dacc92008-10-16 23:51:38566 views::View::ConvertPointToScreen(star_toggle_, &star_location);
initial.commit09911bf2008-07-26 23:55:29567 // Shift the location to make the bubble appear at a visually pleasing
568 // location.
[email protected]96b667d2008-10-14 20:58:44569 gfx::Rect star_bounds(star_location.x(), star_location.y() + 4,
[email protected]6f3bb6c2008-09-17 22:25:33570 star_toggle_->width(),
571 star_toggle_->height());
[email protected]a0dde122008-11-21 20:51:20572 HWND parent = GetWidget()->GetHWND();
initial.commit09911bf2008-07-26 23:55:29573 Profile* profile = model_->profile();
574 GURL url = model_->GetURL(model_index_);
575
576 if (state) {
577 // Only change the star state if the page is not starred. The user can
578 // unstar by way of the bubble.
579 star_toggle_->SetState(true);
580 model_->SetPageStarred(model_index_, true);
581 }
582 // WARNING: if state is true, we've been deleted.
583 BookmarkBubbleView::Show(parent, star_bounds, NULL, profile, url, state);
584}
585
[email protected]c2dacc92008-10-16 23:51:38586void HistoryItemRenderer::LinkActivated(views::Link* link,
initial.commit09911bf2008-07-26 23:55:29587 int event_flags) {
588 if (link == title_link_) {
589 const GURL& url = model_->GetURL(model_index_);
590 PageNavigator* navigator = parent_->navigator();
591 if (navigator && !url.is_empty()) {
592 UserMetrics::RecordAction(L"Destination_History_OpenURL",
593 model_->profile());
[email protected]c0588052008-10-27 23:01:50594 navigator->OpenURL(url, GURL(),
initial.commit09911bf2008-07-26 23:55:29595 event_utils::DispositionFromEventFlags(event_flags),
596 PageTransition::AUTO_BOOKMARK);
597 // WARNING: call to OpenURL likely deleted us.
598 return;
599 }
600 }
601}
602
603HistoryItemRenderer::DragRegion HistoryItemRenderer::GetDragRegion(int x,
604 int y) {
605 // Is the location over the favicon?
606 SkBitmap* favicon = model_->GetFavicon(model_index_);
initial.commit09911bf2008-07-26 23:55:29607 if (favicon) {
608 // If the UI layout is right-to-left, we must make sure we mirror the
609 // favicon position before doing any hit testing.
610 gfx::Rect favicon_bounds;
[email protected]0d8ea702008-10-14 17:03:07611 favicon_bounds.set_x(title_link_->x() - kIconPadding - kFavIconSize);
initial.commit09911bf2008-07-26 23:55:29612 favicon_bounds.set_y(kEntryPadding);
613 favicon_bounds.set_width(favicon->width());
614 favicon_bounds.set_height(favicon->height());
615 favicon_bounds.set_x(MirroredLeftPointForRect(favicon_bounds));
616 if (favicon_bounds.Contains(x, y)) {
617 return FAV_ICON;
618 }
619 }
620
621 // Is it over the thumbnail?
622 if (show_full_ && model_->GetThumbnail(model_index_)) {
623 CRect thumbnail_loc;
624 GetThumbnailBounds(&thumbnail_loc);
625
626 // If the UI layout is right-to-left, we mirror the thumbnail bounds before
627 // we check whether or not it contains the point in question.
628 gfx::Rect mirrored_loc(thumbnail_loc);
629 thumbnail_loc.MoveToX(MirroredLeftPointForRect(mirrored_loc));
630 if (gfx::Rect(thumbnail_loc).Contains(x, y))
631 return THUMBNAIL;
632 }
633
634 return NONE;
635}
636
637HistoryView::HistoryView(SearchableUIContainer* container,
638 BaseHistoryModel* model,
639 PageNavigator* navigator)
640 : container_(container),
641 renderer_(NULL),
642 model_(model),
643 navigator_(navigator),
644 scroll_helper_(this),
645 line_height_(-1),
646 show_results_(false),
647 show_delete_controls_(false),
648 delete_control_width_(0),
649 loading_(true) {
650 DCHECK(model_.get());
651 DCHECK(navigator_);
652 model_->SetObserver(this);
653
654 ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
655 day_break_font_ = resource_bundle.GetFont(ResourceBundle::WebFont);
656
657 // Ensure break_offsets_ is never empty.
658 BreakValue s = {0, 0};
659 break_offsets_.insert(std::make_pair(0, s));
660}
661
662HistoryView::~HistoryView() {
663 if (renderer_)
664 delete renderer_;
665}
666
667void HistoryView::EnsureRenderer() {
668 if (!renderer_)
669 renderer_ = new HistoryItemRenderer(this, show_results_);
670 if (show_delete_controls_ && !delete_renderer_.get()) {
671 delete_renderer_.reset(
[email protected]c2dacc92008-10-16 23:51:38672 new views::Link(
initial.commit09911bf2008-07-26 23:55:29673 l10n_util::GetString(IDS_HISTORY_DELETE_PRIOR_VISITS_LINK)));
674 delete_renderer_->SetFont(day_break_font_);
675 }
676}
677
678int HistoryView::GetLastEntryMaxY() {
679 if (break_offsets_.empty())
680 return 0;
681 BreakOffsets::iterator last_entry_i = break_offsets_.end();
682 last_entry_i--;
683 return last_entry_i->first;
684}
685
686int HistoryView::GetEntryHeight() {
687 if (line_height_ == -1) {
688 ChromeFont font = ResourceBundle::GetSharedInstance()
689 .GetFont(ResourceBundle::WebFont);
690 line_height_ = font.height() + font.height() - font.baseline();
691 }
692 if (show_results_) {
693 return std::max(line_height_ * 3 + kEntryPadding, kSearchResultsHeight);
694 } else {
695 return std::max(line_height_ + kEntryPadding, kBrowseResultsHeight);
696 }
697}
698
699void HistoryView::ModelChanged(bool result_set_changed) {
700 DetachAllFloatingViews();
701
702 if (!result_set_changed) {
703 // Only item metadata changed. We don't need to do a full re-layout,
704 // but we may need to redraw the affected items.
705 SchedulePaint();
706 return;
707 }
708
709 // TODO(evanm): this could be optimized by computing break_offsets_ lazily.
710 // It'd be especially nice because of our incremental search; right now
711 // we recompute the entire layout with each key you press.
712 break_offsets_.clear();
713
714 const int count = model_->GetItemCount();
715
716 // If we're not viewing bookmarks and we are looking at search results, then
717 // show the items in a results (larger) style.
718 show_results_ = model_->IsSearchResults();
719 if (renderer_)
720 renderer_->SetDisplayStyle(show_results_);
721
722 // If we're viewing bookmarks or we're viewing the larger results, we don't
723 // need to insert break offsets between items.
724 if (show_results_) {
725 BreakValue s = {0, true};
726 break_offsets_.insert(std::make_pair(kResultsMargin, s));
727 if (count > 0) {
728 BreakValue s = {count, true};
729 break_offsets_.insert(
730 std::make_pair(GetEntryHeight() * count + kResultsMargin, s));
731 }
732 } else {
733 int y = 0;
734 Time last_time;
735 Time last_day;
736
737 // Loop through our list of items and find places to insert breaks.
738 for (int i = 0; i < count; ++i) {
739 // NOTE: if you change how we calculate breaks you'll need to update
740 // the deletion code as well (DeleteDayAtModelIndex).
741 Time time = model_->GetVisitTime(i);
742 Time day = time.LocalMidnight();
743 if (i == 0 ||
744 (last_time - time).ToInternalValue() > kSessionBreakTime ||
745 day != last_day) {
746 // We've detected something that needs a break.
747
748 bool day_separation = true;
749
750 // If it's not the first item, figure out if it's a day
751 // break or session break.
752 if (i != 0)
753 day_separation = (day != last_day);
754
755 BreakValue s = {i, day_separation};
756
757 break_offsets_.insert(std::make_pair(y, s));
758 y += GetBreakOffsetHeight(s);
759 }
760 last_time = time;
761 last_day = day;
762 y += GetEntryHeight();
763 }
764
765 // Insert ending day.
766 BreakValue s = {count, true};
767 break_offsets_.insert(std::make_pair(y, s));
768 }
769
770 // Find our ScrollView and layout.
771 if (GetParent() && GetParent()->GetParent())
772 GetParent()->GetParent()->Layout();
773}
774
775void HistoryView::ModelBeginWork() {
776 loading_ = true;
777 if (container_)
778 container_->StartThrobber();
779}
780
781void HistoryView::ModelEndWork() {
782 loading_ = false;
783 if (container_)
784 container_->StopThrobber();
785 if (model_->GetItemCount() == 0)
786 SchedulePaint();
787}
788
789void HistoryView::SetShowDeleteControls(bool show_delete_controls) {
790 if (show_delete_controls == show_delete_controls_)
791 return;
792
793 show_delete_controls_ = show_delete_controls;
794
795 delete_renderer_.reset(NULL);
796
797 // Be sure and rebuild the display, otherwise the floating view indices are
798 // off.
799 ModelChanged(true);
800}
801
802int HistoryView::GetPageScrollIncrement(
[email protected]c2dacc92008-10-16 23:51:38803 views::ScrollView* scroll_view, bool is_horizontal,
initial.commit09911bf2008-07-26 23:55:29804 bool is_positive) {
805 return scroll_helper_.GetPageScrollIncrement(scroll_view, is_horizontal,
806 is_positive);
807}
808
809int HistoryView::GetLineScrollIncrement(
[email protected]c2dacc92008-10-16 23:51:38810 views::ScrollView* scroll_view, bool is_horizontal,
initial.commit09911bf2008-07-26 23:55:29811 bool is_positive) {
812 return scroll_helper_.GetLineScrollIncrement(scroll_view, is_horizontal,
813 is_positive);
814}
815
[email protected]c2dacc92008-10-16 23:51:38816views::VariableRowHeightScrollHelper::RowInfo
initial.commit09911bf2008-07-26 23:55:29817 HistoryView::GetRowInfo(int y) {
818 // Get the time separator header for a given Y click.
819 BreakOffsets::iterator i = GetBreakOffsetIteratorForY(y);
820 int index = i->second.index;
821 int current_y = i->first;
822
823 // Check if the click is on the separator header.
824 if (y < current_y + GetBreakOffsetHeight(i->second)) {
[email protected]c2dacc92008-10-16 23:51:38825 return views::VariableRowHeightScrollHelper::RowInfo(
initial.commit09911bf2008-07-26 23:55:29826 current_y, GetBreakOffsetHeight(i->second));
827 }
828
829 // Otherwise increment current_y by the item height until it goes past y.
830 current_y += GetBreakOffsetHeight(i->second);
831
832 while (index < model_->GetItemCount()) {
833 int next_y = current_y + GetEntryHeight();
834 if (y < next_y)
835 break;
836 current_y = next_y;
837 }
838
839 // Find the item that corresponds to this new current_y value.
[email protected]c2dacc92008-10-16 23:51:38840 return views::VariableRowHeightScrollHelper::RowInfo(
initial.commit09911bf2008-07-26 23:55:29841 current_y, GetEntryHeight());
842}
843
844bool HistoryView::IsVisible() {
[email protected]a0dde122008-11-21 20:51:20845 views::Widget* widget = GetWidget();
846 return widget && widget->IsVisible();
initial.commit09911bf2008-07-26 23:55:29847}
848
[email protected]80f8b9f2008-10-16 18:17:47849void HistoryView::DidChangeBounds(const gfx::Rect& previous,
850 const gfx::Rect& current) {
initial.commit09911bf2008-07-26 23:55:29851 SchedulePaint();
852}
853
854void HistoryView::Layout() {
855 DetachAllFloatingViews();
856
857 View* parent = GetParent();
858 if (!parent)
859 return;
860
[email protected]80f8b9f2008-10-16 18:17:47861 gfx::Rect bounds = parent->GetLocalBounds(true);
initial.commit09911bf2008-07-26 23:55:29862
863 // If not visible, have zero size so we don't compute anything.
864 int width = 0;
865 int height = 0;
866 if (IsVisible()) {
[email protected]80f8b9f2008-10-16 18:17:47867 width = bounds.width();
initial.commit09911bf2008-07-26 23:55:29868 height = std::max(GetLastEntryMaxY(),
869 kEntryPadding + kNoResultTextHeight);
870 }
871
[email protected]6f3bb6c2008-09-17 22:25:33872 SetBounds(x(), y(), width, height);
initial.commit09911bf2008-07-26 23:55:29873}
874
875HistoryView::BreakOffsets::iterator HistoryView::GetBreakOffsetIteratorForY(
876 int y) {
877 BreakOffsets::iterator iter = break_offsets_.upper_bound(y);
878 DCHECK(iter != break_offsets_.end());
879 // Move to the first offset smaller than y.
880 if (iter != break_offsets_.begin())
881 --iter;
882 return iter;
883}
884
885int HistoryView::GetBreakOffsetHeight(HistoryView::BreakValue value) {
886 if (show_results_)
887 return 0;
888
889 if (value.day) {
890 return kDayHeadingHeight;
891 } else {
892 return kSessionBreakHeight;
893 }
894}
895
896void HistoryView::Paint(ChromeCanvas* canvas) {
[email protected]c2dacc92008-10-16 23:51:38897 views::View::Paint(canvas);
initial.commit09911bf2008-07-26 23:55:29898
899 EnsureRenderer();
900
901 SkRect clip;
902 if (!canvas->getClipBounds(&clip))
903 return;
904
[email protected]6f3bb6c2008-09-17 22:25:33905 const int content_width = width() - kLeftMargin - kRightMargin;
initial.commit09911bf2008-07-26 23:55:29906
907 const int x1 = kLeftMargin;
908 int clip_y = SkScalarRound(clip.fTop);
909 int clip_max_y = SkScalarRound(clip.fBottom);
910
911 if (model_->GetItemCount() == 0) {
912 // Display text indicating that no results were found.
913 int result_id;
914
915 if (loading_)
916 result_id = IDS_HISTORY_LOADING;
917 else if (show_results_)
918 result_id = IDS_HISTORY_NO_RESULTS;
919 else
920 result_id = IDS_HISTORY_NO_ITEMS;
921
922 canvas->DrawStringInt(l10n_util::GetString(result_id),
923 day_break_font_,
924 SkColorSetRGB(0, 0, 0),
925 x1, kEntryPadding,
926 std::max(content_width, kNoResultMinWidth),
927 kNoResultTextHeight,
928 ChromeCanvas::MULTI_LINE);
929 }
930
931 if (clip_y >= GetLastEntryMaxY())
932 return;
933
934 BreakOffsets::iterator break_offsets_iter =
935 GetBreakOffsetIteratorForY(clip_y);
936 int item_index = break_offsets_iter->second.index;
937 int y = break_offsets_iter->first;
938
939 // Display the "Search results for 'xxxx'" text.
940 if (show_results_ && model_->GetItemCount() > 0) {
941 canvas->DrawStringInt(l10n_util::GetStringF(IDS_HISTORY_SEARCH_STRING,
942 model_->GetSearchText()),
943 day_break_font_,
944 SkColorSetRGB(0, 0, 0),
945 x1, kEntryPadding,
946 content_width, kResultTextHeight,
947 ChromeCanvas::TEXT_VALIGN_BOTTOM);
948 }
949
950 Time midnight_today = Time::Now().LocalMidnight();
951 while (y < clip_max_y && item_index < model_->GetItemCount()) {
952 if (!show_results_ && y == break_offsets_iter->first) {
953 if (y + kDayHeadingHeight > clip_y) {
954 if (break_offsets_iter->second.day) {
955 // We're at a day break, draw the day break appropriately.
956 Time visit_time = model_->GetVisitTime(item_index);
957 DCHECK(visit_time.ToInternalValue() > 0);
958
959 // If it's the first day, then it has a special presentation.
[email protected]5cca3a52008-08-19 22:35:29960 std::wstring date_str = TimeFormat::RelativeDate(visit_time,
961 &midnight_today);
initial.commit09911bf2008-07-26 23:55:29962 if (date_str.empty()) {
[email protected]5cca3a52008-08-19 22:35:29963 date_str = base::TimeFormatFriendlyDate(visit_time);
initial.commit09911bf2008-07-26 23:55:29964 } else {
965 date_str = l10n_util::GetStringF(
966 IDS_HISTORY_DATE_WITH_RELATIVE_TIME,
[email protected]5cca3a52008-08-19 22:35:29967 date_str, base::TimeFormatFriendlyDate(visit_time));
initial.commit09911bf2008-07-26 23:55:29968 }
969
970 // Draw date
971 canvas->DrawStringInt(date_str,
972 day_break_font_,
973 SkColorSetRGB(0, 0, 0),
974 x1, y + kDayHeadingHeight -
975 kBrowseResultsHeight + kEntryPadding,
976 content_width, kBrowseResultsHeight,
977 ChromeCanvas::TEXT_VALIGN_BOTTOM);
978
979 // Draw delete controls.
980 if (show_delete_controls_) {
981 gfx::Rect delete_bounds = CalculateDeleteControlBounds(y);
982 if (!HasFloatingViewForPoint(delete_bounds.x(),
983 delete_bounds.y())) {
984 PaintFloatingView(canvas, delete_renderer_.get(),
985 delete_bounds.x(), delete_bounds.y(),
986 delete_bounds.width(), delete_bounds.height());
987 }
988 }
989 } else {
990 // Draw session separator. Note that we must mirror the position of
991 // the separator if we run in an RTL locale because we draw the
992 // separator directly on the canvas.
993 gfx::Rect separator_bounds(x1 + kSessionGapOffset + kTimeOffset,
994 y,
995 1,
996 kBrowseResultsHeight);
997 separator_bounds.set_x(MirroredLeftPointForRect(separator_bounds));
998 canvas->FillRectInt(SkColorSetRGB(178, 178, 178),
999 separator_bounds.x(), separator_bounds.y(),
1000 separator_bounds.width(),
1001 separator_bounds.height());
1002 }
1003 }
1004
1005 y += GetBreakOffsetHeight(break_offsets_iter->second);
1006 }
1007
1008 if (y + GetEntryHeight() > clip_y && !HasFloatingViewForPoint(x1, y)) {
1009 renderer_->SetModel(model_.get(), item_index);
1010 PaintFloatingView(canvas, renderer_, x1, y, content_width,
1011 GetEntryHeight());
1012 }
1013
1014 y += GetEntryHeight();
1015
1016 BreakOffsets::iterator next_break_offsets = break_offsets_iter;
1017 ++next_break_offsets;
1018 if (next_break_offsets != break_offsets_.end() &&
1019 y >= next_break_offsets->first) {
1020 break_offsets_iter = next_break_offsets;
1021 }
1022
1023 ++item_index;
1024 }
1025}
1026
1027int HistoryView::GetYCoordinateForViewID(int id,
1028 int* model_index,
1029 bool* is_delete_control) {
1030 DCHECK(id < GetMaxViewID());
1031
1032 // Loop through our views and figure out model ids and y coordinates
1033 // of the various items as we go until we find the item that matches.
1034 // the supplied id. This should closely match the code in Paint().
1035 //
1036 // Watch out, this will be is_null when there is no visit.
1037 Time last_time = model_->GetVisitTime(0);
1038
1039 int current_model_index = 0;
1040 int y = show_results_ ? kResultsMargin : 0;
1041
1042 bool show_breaks = !show_results_;
1043
1044 for (int i = 0; i <= id; i++) {
1045 // Consider day and session breaks also between when moving between groups
1046 // of unvisited (visit_time().is_null()) and visited URLs.
1047 Time time = model_->GetVisitTime(current_model_index);
1048 bool at_day_break = last_time.is_null() != time.is_null() ||
1049 (i == 0 || last_time.LocalMidnight() != time.LocalMidnight());
1050 bool at_session_break = last_time.is_null() != time.is_null() ||
1051 (!at_day_break &&
1052 (last_time - time).ToInternalValue() > kSessionBreakTime);
1053 bool at_result = (i == id);
1054
1055 // If we're showing breaks, are a day break and are showing delete
1056 // controls, this view id must be a delete control.
1057 if (show_breaks && at_day_break && show_delete_controls_) {
1058 if (at_result) {
1059 // We've found what we're looking for.
1060 *is_delete_control = true;
1061 *model_index = current_model_index;
1062 return y;
1063 } else {
1064 // This isn't what we're looking for, but it is a valid view, so carry
1065 // on through the loop, but don't increment our current_model_index,
1066 // as the next view will have the same model index.
1067 y += kDayHeadingHeight;
1068 last_time = time;
1069 }
1070 } else {
1071 if (show_breaks) {
1072 if (at_day_break) {
1073 y += kDayHeadingHeight;
1074 } else if (at_session_break) {
1075 y += kSessionBreakHeight;
1076 }
1077 }
1078
1079 // We're on a result item.
1080 if (at_result) {
1081 *is_delete_control = false;
1082 *model_index = current_model_index;
1083 return y;
1084 }
1085
1086 // It wasn't the one we're looking for, so increment our y coordinate and
1087 // model index and move on to the next view.
1088 current_model_index++;
1089 last_time = time;
1090 y += GetEntryHeight();
1091 }
1092 }
1093
1094 return y;
1095}
1096
1097bool HistoryView::GetFloatingViewIDForPoint(int x, int y, int* id) {
1098 // Here's a picture of the various offsets used here.
1099 // Let the (*) on entry #5 below represent the mouse position.
1100 // +--------------
1101 // | entry #2
1102 // +--------------
1103 // <- base_y is the y coordinate of the break.
1104 // +-------------- <- break_offsets->second.index points at this entry
1105 // | entry #3 base_index is this entry index (3).
1106 // +--------------
1107 // +--------------
1108 // | entry #4
1109 // +--------------
1110 // +--------------
1111 // | entry #5 (*) <- y is this y coordinate
1112 // +--------------
1113
1114 // First, verify the x coordinate is within the correct region.
[email protected]6f3bb6c2008-09-17 22:25:331115 if (x < kLeftMargin || x > width() - kRightMargin ||
initial.commit09911bf2008-07-26 23:55:291116 y >= GetLastEntryMaxY()) {
1117 return false;
1118 }
1119
1120 // Find the closest break to this y-coordinate.
1121 BreakOffsets::const_iterator break_offsets_iter =
1122 GetBreakOffsetIteratorForY(y);
1123
1124 // Get the model index of the first item after that break.
1125 int base_index = break_offsets_iter->second.index;
1126
1127 // Get the view id of that item by adding the number of deletes prior to
1128 // this item. (See comments for break_offsets_);
1129 if (show_delete_controls_) {
1130 base_index += CalculateDeleteOffset(break_offsets_iter);
1131
1132 // The current break contains a delete, we need to account for that.
1133 if (break_offsets_iter->second.day)
1134 base_index++;
1135 }
1136
1137 // base_y is the top of the break block.
1138 int base_y = break_offsets_iter->first;
1139
1140 // Add the height of the break.
1141 if (!show_results_)
1142 base_y += GetBreakOffsetHeight(break_offsets_iter->second);
1143
1144 // If y is less than base_y, then it must be over the break and so the
1145 // only view the mouse could be over would be the delete link.
1146 if (y < base_y) {
1147 if (show_delete_controls_ &&
1148 break_offsets_iter->second.day) {
1149 gfx::Rect delete_bounds =
1150 CalculateDeleteControlBounds(base_y - kDayHeadingHeight);
1151
1152 // The delete link bounds must be mirrored if the locale is RTL since the
1153 // point we check against is in LTR coordinates.
1154 delete_bounds.set_x(MirroredLeftPointForRect(delete_bounds));
1155 if (x >= delete_bounds.x() && x < delete_bounds.right()) {
1156 *id = base_index - 1;
1157 return true;
1158 }
1159 }
1160 return false; // Point is over the day heading.
1161 }
1162
1163 // y_delta is the distance from the top of the first item in
1164 // this block to the target y point.
1165 const int y_delta = y - base_y;
1166
1167 int view_id = base_index + (y_delta / GetEntryHeight());
1168 *id = view_id;
1169 return true;
1170}
1171
1172bool HistoryView::EnumerateFloatingViews(
[email protected]c2dacc92008-10-16 23:51:381173 views::View::FloatingViewPosition position,
initial.commit09911bf2008-07-26 23:55:291174 int starting_id,
1175 int* id) {
1176 DCHECK(id);
1177 return View::EnumerateFloatingViewsForInterval(0, GetMaxViewID(),
1178 true,
1179 position, starting_id, id);
1180}
1181
[email protected]c2dacc92008-10-16 23:51:381182views::View* HistoryView::ValidateFloatingViewForID(int id) {
initial.commit09911bf2008-07-26 23:55:291183 if (id >= GetMaxViewID())
1184 return NULL;
1185
1186 bool is_delete_control;
1187 int model_index;
1188 View* floating_view;
1189
1190 int y = GetYCoordinateForViewID(id, &model_index, &is_delete_control);
1191 if (is_delete_control) {
[email protected]c2dacc92008-10-16 23:51:381192 views::Link* delete_link = new views::Link(
initial.commit09911bf2008-07-26 23:55:291193 l10n_util::GetString(IDS_HISTORY_DELETE_PRIOR_VISITS_LINK));
1194 delete_link->SetID(model_index);
1195 delete_link->SetFont(day_break_font_);
1196 delete_link->SetController(this);
1197
1198 gfx::Rect delete_bounds = CalculateDeleteControlBounds(y);
1199 delete_link->SetBounds(delete_bounds.x(), delete_bounds.y(),
1200 delete_bounds.width(), delete_bounds.height());
1201 floating_view = delete_link;
1202 } else {
1203 HistoryItemRenderer* renderer =
1204 new HistoryItemRenderer(this,
1205 show_results_);
1206 renderer->SetModel(model_.get(), model_index);
1207 renderer->SetBounds(kLeftMargin, y,
[email protected]6f3bb6c2008-09-17 22:25:331208 width() - kLeftMargin - kRightMargin,
initial.commit09911bf2008-07-26 23:55:291209 GetEntryHeight());
1210 floating_view = renderer;
1211 }
1212 floating_view->Layout();
1213 AttachFloatingView(floating_view, id);
1214
1215#ifdef DEBUG_FLOATING_VIEWS
[email protected]c2dacc92008-10-16 23:51:381216 floating_view->SetBackground(views::Background::CreateSolidBackground(
initial.commit09911bf2008-07-26 23:55:291217 SkColorSetRGB(255, 0, 0)));
1218 floating_view->SchedulePaint();
1219#endif
1220 return floating_view;
1221}
1222
1223int HistoryView::GetMaxViewID() {
1224 if (!show_delete_controls_)
1225 return model_->GetItemCount();
1226
1227 // Figure out how many delete controls we are displaying.
1228 int deletes = 0;
1229 for (BreakOffsets::iterator i = break_offsets_.begin();
1230 i != break_offsets_.end(); ++i) {
1231 if (i->second.day)
1232 deletes++;
1233 }
1234
1235 // Subtract one because we don't display a delete control at the end.
1236 deletes--;
1237
1238 return std::max(0, deletes + model_->GetItemCount());
1239}
1240
[email protected]c2dacc92008-10-16 23:51:381241void HistoryView::LinkActivated(views::Link* source, int event_flags) {
initial.commit09911bf2008-07-26 23:55:291242 DeleteDayAtModelIndex(source->GetID());
1243}
1244
1245void HistoryView::DeleteDayAtModelIndex(int index) {
1246 std::wstring text = l10n_util::GetString(
1247 IDS_HISTORY_DELETE_PRIOR_VISITS_WARNING);
1248 std::wstring caption = l10n_util::GetString(
1249 IDS_HISTORY_DELETE_PRIOR_VISITS_WARNING_TITLE);
1250 UINT flags = MB_OKCANCEL | MB_ICONWARNING | MB_TOPMOST | MB_SETFOREGROUND;
1251
[email protected]a0dde122008-11-21 20:51:201252 if (win_util::MessageBox(GetWidget()->GetHWND(),
initial.commit09911bf2008-07-26 23:55:291253 text, caption, flags) != IDOK) {
1254 return;
1255 }
1256
[email protected]023b66d12008-09-04 03:35:281257 if (index < 0 || index >= model_->GetItemCount()) {
1258 // Bogus index.
1259 NOTREACHED();
1260 return;
1261 }
1262
initial.commit09911bf2008-07-26 23:55:291263 UserMetrics::RecordAction(L"History_DeleteHistory", model_->profile());
1264
1265 // BrowsingDataRemover deletes itself when done.
1266 // index refers to the last page at the very end of the day, so we delete
1267 // everything after the start of the day and before the end of the day.
1268 Time delete_begin = model_->GetVisitTime(index).LocalMidnight();
1269 Time delete_end =
1270 (model_->GetVisitTime(index) + TimeDelta::FromDays(1)).LocalMidnight();
1271
1272 BrowsingDataRemover* remover =
1273 new BrowsingDataRemover(model_->profile(),
1274 delete_begin,
1275 delete_end);
1276 remover->Remove(BrowsingDataRemover::REMOVE_HISTORY |
1277 BrowsingDataRemover::REMOVE_COOKIES |
1278 BrowsingDataRemover::REMOVE_CACHE);
1279
1280 model_->Refresh();
1281
1282 // Scroll to the origin, otherwise the scroll position isn't changed and the
1283 // user is left looking at a region they originally weren't viewing.
1284 ScrollRectToVisible(0, 0, 0, 0);
1285}
1286
1287int HistoryView::CalculateDeleteOffset(
1288 const BreakOffsets::const_iterator& it) {
1289 DCHECK(show_delete_controls_);
1290 int offset = 0;
1291 for (BreakOffsets::iterator i = break_offsets_.begin(); i != it; ++i) {
1292 if (i->second.day)
1293 offset++;
1294 }
1295 return offset;
1296}
1297
1298int HistoryView::GetDeleteControlWidth() {
1299 if (delete_control_width_)
1300 return delete_control_width_;
initial.commit09911bf2008-07-26 23:55:291301 EnsureRenderer();
[email protected]154f8bc2008-10-15 18:02:301302 gfx::Size pref = delete_renderer_->GetPreferredSize();
1303 delete_control_width_ = pref.width();
initial.commit09911bf2008-07-26 23:55:291304 return delete_control_width_;
1305}
1306
1307gfx::Rect HistoryView::CalculateDeleteControlBounds(int base_y) {
1308 // NOTE: the height here is too big, it should be just big enough to show
1309 // the link. Additionally this should be baseline aligned with the date. I'm
1310 // not doing that now as a redesign of HistoryView is in the works.
1311 const int delete_width = GetDeleteControlWidth();
[email protected]6f3bb6c2008-09-17 22:25:331312 const int delete_x = width() - kRightMargin - delete_width;
initial.commit09911bf2008-07-26 23:55:291313 return gfx::Rect(delete_x,
1314 base_y + kDeleteControlOffset,
1315 delete_width,
1316 kBrowseResultsHeight);
1317}