blob: 128fa871a1200fed4812a4dc243fa438d95373a1 [file] [log] [blame]
// Copyright (c) 2006-2008 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/history_view.h"
#include "base/string_util.h"
#include "base/time_format.h"
#include "base/word_iterator.h"
#include "chrome/browser/browsing_data_remover.h"
#include "chrome/browser/drag_utils.h"
#include "chrome/browser/metrics/user_metrics.h"
#include "chrome/browser/tab_contents/native_ui_contents.h"
#include "chrome/browser/tab_contents/page_navigator.h"
#include "chrome/browser/views/bookmark_bubble_view.h"
#include "chrome/browser/views/event_utils.h"
#include "chrome/browser/views/star_toggle.h"
#include "chrome/common/drag_drop_types.h"
#include "chrome/common/gfx/chrome_canvas.h"
#include "chrome/common/gfx/favicon_size.h"
#include "chrome/common/l10n_util.h"
#include "chrome/common/resource_bundle.h"
#include "chrome/common/time_format.h"
#include "chrome/common/win_util.h"
#include "chrome/views/link.h"
#include "chrome/views/widget.h"
#include "generated_resources.h"
using base::Time;
using base::TimeDelta;
// The extra-wide space between groups of entries for each new day.
static const int kDayHeadingHeight = 50;
// The space between groups of entries within a day.
static const int kSessionBreakHeight = 24;
// Amount of time between page-views that triggers a break (in microseconds).
static const int64 kSessionBreakTime = 1800 * 1000000; // 30 minutes
// Horizontal space between the left edge of the entries and the
// left edge of the view.
static const int kLeftMargin = 38;
// x-position of the page title (massage this so it visually matches
// kDestinationSearchOffset in native_ui_contents.cc
static const int kPageTitleOffset = 102;
// x-position of the Time
static const int kTimeOffset = 24;
// Vertical offset for the delete control (distance from the top of a day
// break segment).
static const int kDeleteControlOffset = 30;
// x-position of the session gap filler (currently a thin vertical line
// joining the times on either side of a session gap).
static const int kSessionGapOffset = 16;
// Horizontal space between the right edge of the item
// and the right edge of the view.
static const int kRightMargin = 20;
// The ideal height of an entry. This may change depending on font line-height.
static const int kSearchResultsHeight = 72;
static const int kBrowseResultsHeight = 24;
// How much room to leave above the first result.
static const int kResultsMargin = 24;
// Height of the results text area.
static const int kResultTextHeight = 24;
// Height of the area when there are no results to display.
static const int kNoResultTextHeight = 48;
static const int kNoResultMinWidth = 512;
// Extra vertical space between the different lines of text.
// (Note that the height() variables are baseline-to-baseline already.)
static const int kLeading = 2;
// The amount of space from the edges of an entry to the edges of its contents.
static const int kEntryPadding = 8;
// Padding between the icons (star, favicon) and other elements.
static const int kIconPadding = 4;
// SnippetRenderer is a View that can displayed text with bolding and wrapping.
// It's used to display search result snippets.
class SnippetRenderer : public views::View {
public:
SnippetRenderer();
// Set the text snippet.
void SetSnippet(const Snippet& snippet);
int GetLineHeight();
virtual void Paint(ChromeCanvas* canvas);
private:
// The snippet that we're drawing.
Snippet snippet_;
// Font for plain text.
ChromeFont text_font_;
// Font for match text. (TODO(evanm): use red for Chinese (bug 844518).)
ChromeFont match_font_;
// Layout/draw a substring of the snippet from [start,end) at (x, y).
// ProcessRun is strictly for text in a single line: it doesn't do any
// word-wrapping, and is used as a helper for laying out multiple lines
// of output in Pain().
// match_iter is an iterator in match_runs_ that covers a region
// before or at start.
// When canvas is NULL, does no drawing and only computes the size.
// Returns the pixel width of the run.
// TODO(evanm): this could be optimizing by only measuring the text once
// and returning the layout, but it's worth profiling first.
int ProcessRun(ChromeCanvas* canvas,
int x,
int y,
Snippet::MatchPositions::const_iterator match_iter,
size_t start,
size_t end);
DISALLOW_EVIL_CONSTRUCTORS(SnippetRenderer);
};
SnippetRenderer::SnippetRenderer() {
ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
text_font_ = resource_bundle.GetFont(ResourceBundle::WebFont);
match_font_ = text_font_.DeriveFont(0, ChromeFont::BOLD);
}
void SnippetRenderer::SetSnippet(const Snippet& snippet) {
snippet_ = snippet;
}
int SnippetRenderer::GetLineHeight() {
return std::max(text_font_.height(), match_font_.height()) + kLeading;
}
void SnippetRenderer::Paint(ChromeCanvas* canvas) {
const int line_height = GetLineHeight();
WordIterator iter(snippet_.text(), WordIterator::BREAK_LINE);
if (!iter.Init())
return;
Snippet::MatchPositions::const_iterator match_iter =
snippet_.matches().begin();
int x = 0;
int y = 0;
while (iter.Advance()) {
// Advance match_iter to a run that potentially covers this region.
while (match_iter != snippet_.matches().end() &&
match_iter->second <= iter.prev()) {
++match_iter;
}
// The region from iter.prev() to iter.pos() should be on one line.
// It can be a mixture of bold and non-bold, so first lay it out to
// compute its width.
const int width = ProcessRun(NULL, 0, 0,
match_iter, iter.prev(), iter.pos());
// Advance to the next line if necessary.
if (x + width > View::width()) {
x = 0;
y += line_height;
if (y >= height())
return; // Out of vertical space.
}
ProcessRun(canvas, x, y, match_iter, iter.prev(), iter.pos());
x += width;
}
}
int SnippetRenderer::ProcessRun(
ChromeCanvas* canvas,
int x,
int y,
Snippet::MatchPositions::const_iterator match_iter,
size_t start,
size_t end) {
int total_width = 0;
while (start < end) {
// Advance match_iter to the next match that can cover the current
// position.
while (match_iter != snippet_.matches().end() &&
match_iter->second <= start) {
++match_iter;
}
// Determine the next substring to process by examining whether
// we're before a match or within a match.
ChromeFont* font = &text_font_;
size_t next = end;
if (match_iter != snippet_.matches().end()) {
if (match_iter->first > start) {
// We're in a plain region.
next = std::min(match_iter->first, end);
} else if (match_iter->first <= start &&
match_iter->second > start) {
// We're in a match region.
font = &match_font_;
next = std::min(match_iter->second, end);
}
}
// Draw/layout the text.
const std::wstring run = snippet_.text().substr(start, next - start);
const int width = font->GetStringWidth(run);
if (canvas) {
canvas->DrawStringInt(run, *font, SkColorSetRGB(0, 0, 0),
x + total_width, y,
width, height(),
ChromeCanvas::TEXT_VALIGN_BOTTOM);
}
// Advance.
total_width += width;
start = next;
}
return total_width;
}
// A View for an individual history result.
class HistoryItemRenderer : public views::View,
public views::LinkController,
public StarToggle::Delegate {
public:
HistoryItemRenderer(HistoryView* parent, bool show_full);
~HistoryItemRenderer();
// Set the BaseHistoryModel that this renderer displays.
// model_index is the index of this entry, and is passed to all of the
// model functions.
void SetModel(BaseHistoryModel* model, int model_index);
// Set whether we should display full size or partial-sized items.
void SetDisplayStyle(bool show_full);
// Layout the contents of this view.
void Layout();
protected:
// Overridden to do a drag if over the favicon or thumbnail.
virtual int GetDragOperations(int press_x, int press_y);
virtual void WriteDragData(int press_x, int press_y, OSExchangeData* data);
private:
// Regions drags may originate from.
enum DragRegion {
FAV_ICON,
THUMBNAIL,
NONE
};
// The thickness of the border drawn around thumbnails.
static const int kThumbnailBorderWidth = 1;
// The height of the thumbnail images.
static const int kThumbnailHeight = kSearchResultsHeight - kEntryPadding * 2;
// The width of the thumbnail images.
static const int kThumbnailWidth = static_cast<int>(1.44 * kThumbnailHeight);
// The maximum width of a snippet - we want to constrain this to make
// snippets easier to read (like Google search results).
static const int kMaxSnippetWidth = 500;
// Returns the bounds of the thumbnail.
void GetThumbnailBounds(CRect* rect);
// Convert a GURL into a displayable string.
std::wstring DisplayURL(const GURL& url);
virtual void Paint(ChromeCanvas* canvas);
// Notification that the star was changed.
virtual void StarStateChanged(bool state);
// Notification that the link was clicked.
virtual void LinkActivated(views::Link* source, int event_flags);
// Returns the region the mouse is over.
DragRegion GetDragRegion(int x, int y);
// The HistoryView containing this view.
HistoryView* parent_;
// Whether we're showing a fullsize item, or a single-line item.
bool show_full_;
// The model and index of this entry within the model.
BaseHistoryModel* model_;
int model_index_;
// Widgets.
StarToggle* star_toggle_;
views::Link* title_link_;
views::Label* time_label_;
SnippetRenderer* snippet_label_;
DISALLOW_EVIL_CONSTRUCTORS(HistoryItemRenderer);
};
HistoryItemRenderer::HistoryItemRenderer(HistoryView* parent,
bool show_full)
: parent_(parent),
show_full_(show_full) {
ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
ChromeFont text_font(resource_bundle.GetFont(ResourceBundle::WebFont));
star_toggle_ = new StarToggle(this);
star_toggle_->set_change_state_immediately(false);
AddChildView(star_toggle_);
title_link_ = new views::Link();
title_link_->SetFont(text_font);
title_link_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
title_link_->SetController(this);
AddChildView(title_link_);
const SkColor kTimeColor = SkColorSetRGB(136, 136, 136); // Gray.
time_label_ = new views::Label();
ChromeFont time_font(text_font);
time_label_->SetFont(time_font);
time_label_->SetColor(kTimeColor);
time_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
AddChildView(time_label_);
snippet_label_ = new SnippetRenderer();
AddChildView(snippet_label_);
}
HistoryItemRenderer::~HistoryItemRenderer() {
}
void HistoryItemRenderer::GetThumbnailBounds(CRect* rect) {
DCHECK(rect);
rect->right = width() - kEntryPadding;
rect->left = rect->right - kThumbnailWidth;
rect->top = kEntryPadding;
rect->bottom = rect->top + kThumbnailHeight;
}
std::wstring HistoryItemRenderer::DisplayURL(const GURL& url) {
std::string url_str = url.spec();
// Hide the "http://" prefix like web search does.
if (url_str.find("http://") == 0)
url_str.erase(0, strlen("http://"));
return UTF8ToWide(url_str);
}
void HistoryItemRenderer::Paint(ChromeCanvas* canvas) {
views::View::Paint(canvas);
// Draw thumbnail or placeholder.
if (show_full_) {
SkBitmap* thumbnail = model_->GetThumbnail(model_index_);
CRect thumbnail_rect;
GetThumbnailBounds(&thumbnail_rect); // Includes border
// If the UI layout is right-to-left, we must mirror the bounds so that we
// render the bitmap in the correct position.
gfx::Rect mirrored_rect(thumbnail_rect);
thumbnail_rect.MoveToX(MirroredLeftPointForRect(mirrored_rect));
if (thumbnail) {
// This will create a MipMap for the bitmap if one doesn't exist already
// (it's a NOP if a MipMap already exists). This will give much smoother
// results for the scaled-down thumbnails.
thumbnail->buildMipMap(false);
canvas->DrawBitmapInt(
*thumbnail,
0, 0, thumbnail->width(), thumbnail->height(),
thumbnail_rect.left, thumbnail_rect.top,
thumbnail_rect.Width(), thumbnail_rect.Height(),
true);
} else {
canvas->FillRectInt(SK_ColorWHITE,
thumbnail_rect.left, thumbnail_rect.top,
thumbnail_rect.Width(), thumbnail_rect.Height());
}
canvas->DrawRectInt(SkColorSetRGB(153, 153, 191),
thumbnail_rect.left, thumbnail_rect.top,
thumbnail_rect.Width(), thumbnail_rect.Height());
}
// Draw the favicon.
SkBitmap* favicon = model_->GetFavicon(model_index_);
if (favicon) {
// WARNING: if you change these values, update the code that determines
// whether we should allow a drag (GetDragRegion).
// We need to tweak the favicon position if the UI layout is RTL.
gfx::Rect favicon_bounds;
favicon_bounds.set_x(title_link_->x() - kIconPadding - kFavIconSize);
favicon_bounds.set_y(kEntryPadding);
favicon_bounds.set_width(favicon->width());
favicon_bounds.set_height(favicon->height());
favicon_bounds.set_x(MirroredLeftPointForRect(favicon_bounds));
// Drawing the bitmap using the possibly adjusted bounds.
canvas->DrawBitmapInt(*favicon, favicon_bounds.x(), favicon_bounds.y());
}
// The remainder of painting is handled by drawing our children, which
// is managed by the View class for us.
}
void HistoryItemRenderer::Layout() {
// Figure out the maximum x-position of any text.
CRect thumbnail_rect;
int max_x;
if (show_full_) {
GetThumbnailBounds(&thumbnail_rect);
max_x = thumbnail_rect.left - kEntryPadding;
} else {
max_x = width() - kEntryPadding;
}
// Calculate the ideal positions of some items. If possible, we
// want the title to line up with kPageTitleOffset (and we would lay
// out the star and the favicon to the left of that), but in cases
// where font or language choices cause the time label to be
// horizontally large, we need to push everything to the right.
//
// If you fiddle with the calculations below, you may need to adjust
// the favicon painting in Paint() (and in GetDragRegion by extension).
// First we calculate the ideal position of the title.
int title_x = kPageTitleOffset;
// We calculate the size of the star.
gfx::Size star_size = star_toggle_->GetPreferredSize();
// Measure and lay out the time label, and potentially move
// our title to suit.
Time visit_time = model_->GetVisitTime(model_index_);
int time_x = kTimeOffset;
if (visit_time.is_null()) {
// We will get null times if the page has never been visited, for example,
// bookmarks after you clear history.
time_label_->SetText(std::wstring());
} else if (show_full_) {
time_x = 0;
time_label_->SetText(base::TimeFormatShortDate(visit_time));
} else {
time_label_->SetText(base::TimeFormatTimeOfDay(visit_time));
}
gfx::Size time_size = time_label_->GetPreferredSize();
time_label_->SetBounds(time_x, kEntryPadding,
time_size.width(), time_size.height());
// Calculate the position of the favicon.
int favicon_x = title_x - kFavIconSize - kIconPadding;
// Now we look to see if the favicon overlaps the time label,
// and if so, we push the title to the right. If we're not
// showing the time label, then ignore this step.
int overlap = favicon_x - (time_x + time_size.width() + kIconPadding);
if (overlap < 0) {
title_x -= overlap;
}
// Populate and measure the title label.
const std::wstring& title = model_->GetTitle(model_index_);
if (!title.empty())
title_link_->SetText(title);
else
title_link_->SetText(l10n_util::GetString(IDS_HISTORY_UNTITLED_TITLE));
gfx::Size title_size = title_link_->GetPreferredSize();
// Lay out the title label.
int max_title_x;
max_title_x = std::max(0, max_x - title_x);
if (title_size.width() + kEntryPadding > max_title_x) {
// We need to shrink the title to make everything fit.
title_size.set_width(max_title_x - kEntryPadding);
}
title_link_->SetBounds(title_x, kEntryPadding,
title_size.width(), title_size.height());
// Lay out the star.
if (model_->IsStarred(model_index_)) {
star_toggle_->SetBounds(title_x + title_size.width() + kIconPadding,
kEntryPadding, star_size.width(),
star_size.height());
star_toggle_->SetState(true);
star_toggle_->SetVisible(true);
} else {
star_toggle_->SetVisible(false);
}
// Lay out the snippet label.
snippet_label_->SetVisible(show_full_);
if (show_full_) {
const Snippet& snippet = model_->GetSnippet(model_index_);
if (snippet.text().empty()) {
snippet_label_->SetSnippet(Snippet()); // Bug 843469 will fix this.
} else {
snippet_label_->SetSnippet(snippet);
}
snippet_label_->SetBounds(title_x,
kEntryPadding + snippet_label_->GetLineHeight(),
std::min(
static_cast<int>(thumbnail_rect.left -
title_x),
kMaxSnippetWidth) -
kEntryPadding * 2,
snippet_label_->GetLineHeight() * 2);
}
}
int HistoryItemRenderer::GetDragOperations(int x, int y) {
if (GetDragRegion(x, y) != NONE)
return DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_LINK;
return DragDropTypes::DRAG_NONE;
}
void HistoryItemRenderer::WriteDragData(int press_x,
int press_y,
OSExchangeData* data) {
DCHECK(GetDragOperations(press_x, press_y) != DragDropTypes::DRAG_NONE);
if (GetDragRegion(press_x, press_y) == FAV_ICON)
UserMetrics::RecordAction(L"History_DragIcon", model_->profile());
else
UserMetrics::RecordAction(L"History_DragThumbnail", model_->profile());
SkBitmap icon;
if (model_->GetFavicon(model_index_))
icon = *model_->GetFavicon(model_index_);
drag_utils::SetURLAndDragImage(model_->GetURL(model_index_),
model_->GetTitle(model_index_),
icon, data);
}
void HistoryItemRenderer::SetModel(BaseHistoryModel* model, int model_index) {
DCHECK(model_index < model->GetItemCount());
model_ = model;
model_index_ = model_index;
}
void HistoryItemRenderer::SetDisplayStyle(bool show_full) {
show_full_ = show_full;
}
void HistoryItemRenderer::StarStateChanged(bool state) {
// Show the user a tip that can be used to edit the bookmark/star.
gfx::Point star_location;
views::View::ConvertPointToScreen(star_toggle_, &star_location);
// Shift the location to make the bubble appear at a visually pleasing
// location.
gfx::Rect star_bounds(star_location.x(), star_location.y() + 4,
star_toggle_->width(),
star_toggle_->height());
HWND parent = GetWidget()->GetHWND();
Profile* profile = model_->profile();
GURL url = model_->GetURL(model_index_);
if (state) {
// Only change the star state if the page is not starred. The user can
// unstar by way of the bubble.
star_toggle_->SetState(true);
model_->SetPageStarred(model_index_, true);
}
// WARNING: if state is true, we've been deleted.
BookmarkBubbleView::Show(parent, star_bounds, NULL, profile, url, state);
}
void HistoryItemRenderer::LinkActivated(views::Link* link,
int event_flags) {
if (link == title_link_) {
const GURL& url = model_->GetURL(model_index_);
PageNavigator* navigator = parent_->navigator();
if (navigator && !url.is_empty()) {
UserMetrics::RecordAction(L"Destination_History_OpenURL",
model_->profile());
navigator->OpenURL(url, GURL(),
event_utils::DispositionFromEventFlags(event_flags),
PageTransition::AUTO_BOOKMARK);
// WARNING: call to OpenURL likely deleted us.
return;
}
}
}
HistoryItemRenderer::DragRegion HistoryItemRenderer::GetDragRegion(int x,
int y) {
// Is the location over the favicon?
SkBitmap* favicon = model_->GetFavicon(model_index_);
if (favicon) {
// If the UI layout is right-to-left, we must make sure we mirror the
// favicon position before doing any hit testing.
gfx::Rect favicon_bounds;
favicon_bounds.set_x(title_link_->x() - kIconPadding - kFavIconSize);
favicon_bounds.set_y(kEntryPadding);
favicon_bounds.set_width(favicon->width());
favicon_bounds.set_height(favicon->height());
favicon_bounds.set_x(MirroredLeftPointForRect(favicon_bounds));
if (favicon_bounds.Contains(x, y)) {
return FAV_ICON;
}
}
// Is it over the thumbnail?
if (show_full_ && model_->GetThumbnail(model_index_)) {
CRect thumbnail_loc;
GetThumbnailBounds(&thumbnail_loc);
// If the UI layout is right-to-left, we mirror the thumbnail bounds before
// we check whether or not it contains the point in question.
gfx::Rect mirrored_loc(thumbnail_loc);
thumbnail_loc.MoveToX(MirroredLeftPointForRect(mirrored_loc));
if (gfx::Rect(thumbnail_loc).Contains(x, y))
return THUMBNAIL;
}
return NONE;
}
HistoryView::HistoryView(SearchableUIContainer* container,
BaseHistoryModel* model,
PageNavigator* navigator)
: container_(container),
renderer_(NULL),
model_(model),
navigator_(navigator),
scroll_helper_(this),
line_height_(-1),
show_results_(false),
show_delete_controls_(false),
delete_control_width_(0),
loading_(true) {
DCHECK(model_.get());
DCHECK(navigator_);
model_->SetObserver(this);
ResourceBundle& resource_bundle = ResourceBundle::GetSharedInstance();
day_break_font_ = resource_bundle.GetFont(ResourceBundle::WebFont);
// Ensure break_offsets_ is never empty.
BreakValue s = {0, 0};
break_offsets_.insert(std::make_pair(0, s));
}
HistoryView::~HistoryView() {
if (renderer_)
delete renderer_;
}
void HistoryView::EnsureRenderer() {
if (!renderer_)
renderer_ = new HistoryItemRenderer(this, show_results_);
if (show_delete_controls_ && !delete_renderer_.get()) {
delete_renderer_.reset(
new views::Link(
l10n_util::GetString(IDS_HISTORY_DELETE_PRIOR_VISITS_LINK)));
delete_renderer_->SetFont(day_break_font_);
}
}
int HistoryView::GetLastEntryMaxY() {
if (break_offsets_.empty())
return 0;
BreakOffsets::iterator last_entry_i = break_offsets_.end();
last_entry_i--;
return last_entry_i->first;
}
int HistoryView::GetEntryHeight() {
if (line_height_ == -1) {
ChromeFont font = ResourceBundle::GetSharedInstance()
.GetFont(ResourceBundle::WebFont);
line_height_ = font.height() + font.height() - font.baseline();
}
if (show_results_) {
return std::max(line_height_ * 3 + kEntryPadding, kSearchResultsHeight);
} else {
return std::max(line_height_ + kEntryPadding, kBrowseResultsHeight);
}
}
void HistoryView::ModelChanged(bool result_set_changed) {
DetachAllFloatingViews();
if (!result_set_changed) {
// Only item metadata changed. We don't need to do a full re-layout,
// but we may need to redraw the affected items.
SchedulePaint();
return;
}
// TODO(evanm): this could be optimized by computing break_offsets_ lazily.
// It'd be especially nice because of our incremental search; right now
// we recompute the entire layout with each key you press.
break_offsets_.clear();
const int count = model_->GetItemCount();
// If we're not viewing bookmarks and we are looking at search results, then
// show the items in a results (larger) style.
show_results_ = model_->IsSearchResults();
if (renderer_)
renderer_->SetDisplayStyle(show_results_);
// If we're viewing bookmarks or we're viewing the larger results, we don't
// need to insert break offsets between items.
if (show_results_) {
BreakValue s = {0, true};
break_offsets_.insert(std::make_pair(kResultsMargin, s));
if (count > 0) {
BreakValue s = {count, true};
break_offsets_.insert(
std::make_pair(GetEntryHeight() * count + kResultsMargin, s));
}
} else {
int y = 0;
Time last_time;
Time last_day;
// Loop through our list of items and find places to insert breaks.
for (int i = 0; i < count; ++i) {
// NOTE: if you change how we calculate breaks you'll need to update
// the deletion code as well (DeleteDayAtModelIndex).
Time time = model_->GetVisitTime(i);
Time day = time.LocalMidnight();
if (i == 0 ||
(last_time - time).ToInternalValue() > kSessionBreakTime ||
day != last_day) {
// We've detected something that needs a break.
bool day_separation = true;
// If it's not the first item, figure out if it's a day
// break or session break.
if (i != 0)
day_separation = (day != last_day);
BreakValue s = {i, day_separation};
break_offsets_.insert(std::make_pair(y, s));
y += GetBreakOffsetHeight(s);
}
last_time = time;
last_day = day;
y += GetEntryHeight();
}
// Insert ending day.
BreakValue s = {count, true};
break_offsets_.insert(std::make_pair(y, s));
}
// Find our ScrollView and layout.
if (GetParent() && GetParent()->GetParent())
GetParent()->GetParent()->Layout();
}
void HistoryView::ModelBeginWork() {
loading_ = true;
if (container_)
container_->StartThrobber();
}
void HistoryView::ModelEndWork() {
loading_ = false;
if (container_)
container_->StopThrobber();
if (model_->GetItemCount() == 0)
SchedulePaint();
}
void HistoryView::SetShowDeleteControls(bool show_delete_controls) {
if (show_delete_controls == show_delete_controls_)
return;
show_delete_controls_ = show_delete_controls;
delete_renderer_.reset(NULL);
// Be sure and rebuild the display, otherwise the floating view indices are
// off.
ModelChanged(true);
}
int HistoryView::GetPageScrollIncrement(
views::ScrollView* scroll_view, bool is_horizontal,
bool is_positive) {
return scroll_helper_.GetPageScrollIncrement(scroll_view, is_horizontal,
is_positive);
}
int HistoryView::GetLineScrollIncrement(
views::ScrollView* scroll_view, bool is_horizontal,
bool is_positive) {
return scroll_helper_.GetLineScrollIncrement(scroll_view, is_horizontal,
is_positive);
}
views::VariableRowHeightScrollHelper::RowInfo
HistoryView::GetRowInfo(int y) {
// Get the time separator header for a given Y click.
BreakOffsets::iterator i = GetBreakOffsetIteratorForY(y);
int index = i->second.index;
int current_y = i->first;
// Check if the click is on the separator header.
if (y < current_y + GetBreakOffsetHeight(i->second)) {
return views::VariableRowHeightScrollHelper::RowInfo(
current_y, GetBreakOffsetHeight(i->second));
}
// Otherwise increment current_y by the item height until it goes past y.
current_y += GetBreakOffsetHeight(i->second);
while (index < model_->GetItemCount()) {
int next_y = current_y + GetEntryHeight();
if (y < next_y)
break;
current_y = next_y;
}
// Find the item that corresponds to this new current_y value.
return views::VariableRowHeightScrollHelper::RowInfo(
current_y, GetEntryHeight());
}
bool HistoryView::IsVisible() {
views::Widget* widget = GetWidget();
return widget && widget->IsVisible();
}
void HistoryView::DidChangeBounds(const gfx::Rect& previous,
const gfx::Rect& current) {
SchedulePaint();
}
void HistoryView::Layout() {
DetachAllFloatingViews();
View* parent = GetParent();
if (!parent)
return;
gfx::Rect bounds = parent->GetLocalBounds(true);
// If not visible, have zero size so we don't compute anything.
int width = 0;
int height = 0;
if (IsVisible()) {
width = bounds.width();
height = std::max(GetLastEntryMaxY(),
kEntryPadding + kNoResultTextHeight);
}
SetBounds(x(), y(), width, height);
}
HistoryView::BreakOffsets::iterator HistoryView::GetBreakOffsetIteratorForY(
int y) {
BreakOffsets::iterator iter = break_offsets_.upper_bound(y);
DCHECK(iter != break_offsets_.end());
// Move to the first offset smaller than y.
if (iter != break_offsets_.begin())
--iter;
return iter;
}
int HistoryView::GetBreakOffsetHeight(HistoryView::BreakValue value) {
if (show_results_)
return 0;
if (value.day) {
return kDayHeadingHeight;
} else {
return kSessionBreakHeight;
}
}
void HistoryView::Paint(ChromeCanvas* canvas) {
views::View::Paint(canvas);
EnsureRenderer();
SkRect clip;
if (!canvas->getClipBounds(&clip))
return;
const int content_width = width() - kLeftMargin - kRightMargin;
const int x1 = kLeftMargin;
int clip_y = SkScalarRound(clip.fTop);
int clip_max_y = SkScalarRound(clip.fBottom);
if (model_->GetItemCount() == 0) {
// Display text indicating that no results were found.
int result_id;
if (loading_)
result_id = IDS_HISTORY_LOADING;
else if (show_results_)
result_id = IDS_HISTORY_NO_RESULTS;
else
result_id = IDS_HISTORY_NO_ITEMS;
canvas->DrawStringInt(l10n_util::GetString(result_id),
day_break_font_,
SkColorSetRGB(0, 0, 0),
x1, kEntryPadding,
std::max(content_width, kNoResultMinWidth),
kNoResultTextHeight,
ChromeCanvas::MULTI_LINE);
}
if (clip_y >= GetLastEntryMaxY())
return;
BreakOffsets::iterator break_offsets_iter =
GetBreakOffsetIteratorForY(clip_y);
int item_index = break_offsets_iter->second.index;
int y = break_offsets_iter->first;
// Display the "Search results for 'xxxx'" text.
if (show_results_ && model_->GetItemCount() > 0) {
canvas->DrawStringInt(l10n_util::GetStringF(IDS_HISTORY_SEARCH_STRING,
model_->GetSearchText()),
day_break_font_,
SkColorSetRGB(0, 0, 0),
x1, kEntryPadding,
content_width, kResultTextHeight,
ChromeCanvas::TEXT_VALIGN_BOTTOM);
}
Time midnight_today = Time::Now().LocalMidnight();
while (y < clip_max_y && item_index < model_->GetItemCount()) {
if (!show_results_ && y == break_offsets_iter->first) {
if (y + kDayHeadingHeight > clip_y) {
if (break_offsets_iter->second.day) {
// We're at a day break, draw the day break appropriately.
Time visit_time = model_->GetVisitTime(item_index);
DCHECK(visit_time.ToInternalValue() > 0);
// If it's the first day, then it has a special presentation.
std::wstring date_str = TimeFormat::RelativeDate(visit_time,
&midnight_today);
if (date_str.empty()) {
date_str = base::TimeFormatFriendlyDate(visit_time);
} else {
date_str = l10n_util::GetStringF(
IDS_HISTORY_DATE_WITH_RELATIVE_TIME,
date_str, base::TimeFormatFriendlyDate(visit_time));
}
// Draw date
canvas->DrawStringInt(date_str,
day_break_font_,
SkColorSetRGB(0, 0, 0),
x1, y + kDayHeadingHeight -
kBrowseResultsHeight + kEntryPadding,
content_width, kBrowseResultsHeight,
ChromeCanvas::TEXT_VALIGN_BOTTOM);
// Draw delete controls.
if (show_delete_controls_) {
gfx::Rect delete_bounds = CalculateDeleteControlBounds(y);
if (!HasFloatingViewForPoint(delete_bounds.x(),
delete_bounds.y())) {
PaintFloatingView(canvas, delete_renderer_.get(),
delete_bounds.x(), delete_bounds.y(),
delete_bounds.width(), delete_bounds.height());
}
}
} else {
// Draw session separator. Note that we must mirror the position of
// the separator if we run in an RTL locale because we draw the
// separator directly on the canvas.
gfx::Rect separator_bounds(x1 + kSessionGapOffset + kTimeOffset,
y,
1,
kBrowseResultsHeight);
separator_bounds.set_x(MirroredLeftPointForRect(separator_bounds));
canvas->FillRectInt(SkColorSetRGB(178, 178, 178),
separator_bounds.x(), separator_bounds.y(),
separator_bounds.width(),
separator_bounds.height());
}
}
y += GetBreakOffsetHeight(break_offsets_iter->second);
}
if (y + GetEntryHeight() > clip_y && !HasFloatingViewForPoint(x1, y)) {
renderer_->SetModel(model_.get(), item_index);
PaintFloatingView(canvas, renderer_, x1, y, content_width,
GetEntryHeight());
}
y += GetEntryHeight();
BreakOffsets::iterator next_break_offsets = break_offsets_iter;
++next_break_offsets;
if (next_break_offsets != break_offsets_.end() &&
y >= next_break_offsets->first) {
break_offsets_iter = next_break_offsets;
}
++item_index;
}
}
int HistoryView::GetYCoordinateForViewID(int id,
int* model_index,
bool* is_delete_control) {
DCHECK(id < GetMaxViewID());
// Loop through our views and figure out model ids and y coordinates
// of the various items as we go until we find the item that matches.
// the supplied id. This should closely match the code in Paint().
//
// Watch out, this will be is_null when there is no visit.
Time last_time = model_->GetVisitTime(0);
int current_model_index = 0;
int y = show_results_ ? kResultsMargin : 0;
bool show_breaks = !show_results_;
for (int i = 0; i <= id; i++) {
// Consider day and session breaks also between when moving between groups
// of unvisited (visit_time().is_null()) and visited URLs.
Time time = model_->GetVisitTime(current_model_index);
bool at_day_break = last_time.is_null() != time.is_null() ||
(i == 0 || last_time.LocalMidnight() != time.LocalMidnight());
bool at_session_break = last_time.is_null() != time.is_null() ||
(!at_day_break &&
(last_time - time).ToInternalValue() > kSessionBreakTime);
bool at_result = (i == id);
// If we're showing breaks, are a day break and are showing delete
// controls, this view id must be a delete control.
if (show_breaks && at_day_break && show_delete_controls_) {
if (at_result) {
// We've found what we're looking for.
*is_delete_control = true;
*model_index = current_model_index;
return y;
} else {
// This isn't what we're looking for, but it is a valid view, so carry
// on through the loop, but don't increment our current_model_index,
// as the next view will have the same model index.
y += kDayHeadingHeight;
last_time = time;
}
} else {
if (show_breaks) {
if (at_day_break) {
y += kDayHeadingHeight;
} else if (at_session_break) {
y += kSessionBreakHeight;
}
}
// We're on a result item.
if (at_result) {
*is_delete_control = false;
*model_index = current_model_index;
return y;
}
// It wasn't the one we're looking for, so increment our y coordinate and
// model index and move on to the next view.
current_model_index++;
last_time = time;
y += GetEntryHeight();
}
}
return y;
}
bool HistoryView::GetFloatingViewIDForPoint(int x, int y, int* id) {
// Here's a picture of the various offsets used here.
// Let the (*) on entry #5 below represent the mouse position.
// +--------------
// | entry #2
// +--------------
// <- base_y is the y coordinate of the break.
// +-------------- <- break_offsets->second.index points at this entry
// | entry #3 base_index is this entry index (3).
// +--------------
// +--------------
// | entry #4
// +--------------
// +--------------
// | entry #5 (*) <- y is this y coordinate
// +--------------
// First, verify the x coordinate is within the correct region.
if (x < kLeftMargin || x > width() - kRightMargin ||
y >= GetLastEntryMaxY()) {
return false;
}
// Find the closest break to this y-coordinate.
BreakOffsets::const_iterator break_offsets_iter =
GetBreakOffsetIteratorForY(y);
// Get the model index of the first item after that break.
int base_index = break_offsets_iter->second.index;
// Get the view id of that item by adding the number of deletes prior to
// this item. (See comments for break_offsets_);
if (show_delete_controls_) {
base_index += CalculateDeleteOffset(break_offsets_iter);
// The current break contains a delete, we need to account for that.
if (break_offsets_iter->second.day)
base_index++;
}
// base_y is the top of the break block.
int base_y = break_offsets_iter->first;
// Add the height of the break.
if (!show_results_)
base_y += GetBreakOffsetHeight(break_offsets_iter->second);
// If y is less than base_y, then it must be over the break and so the
// only view the mouse could be over would be the delete link.
if (y < base_y) {
if (show_delete_controls_ &&
break_offsets_iter->second.day) {
gfx::Rect delete_bounds =
CalculateDeleteControlBounds(base_y - kDayHeadingHeight);
// The delete link bounds must be mirrored if the locale is RTL since the
// point we check against is in LTR coordinates.
delete_bounds.set_x(MirroredLeftPointForRect(delete_bounds));
if (x >= delete_bounds.x() && x < delete_bounds.right()) {
*id = base_index - 1;
return true;
}
}
return false; // Point is over the day heading.
}
// y_delta is the distance from the top of the first item in
// this block to the target y point.
const int y_delta = y - base_y;
int view_id = base_index + (y_delta / GetEntryHeight());
*id = view_id;
return true;
}
bool HistoryView::EnumerateFloatingViews(
views::View::FloatingViewPosition position,
int starting_id,
int* id) {
DCHECK(id);
return View::EnumerateFloatingViewsForInterval(0, GetMaxViewID(),
true,
position, starting_id, id);
}
views::View* HistoryView::ValidateFloatingViewForID(int id) {
if (id >= GetMaxViewID())
return NULL;
bool is_delete_control;
int model_index;
View* floating_view;
int y = GetYCoordinateForViewID(id, &model_index, &is_delete_control);
if (is_delete_control) {
views::Link* delete_link = new views::Link(
l10n_util::GetString(IDS_HISTORY_DELETE_PRIOR_VISITS_LINK));
delete_link->SetID(model_index);
delete_link->SetFont(day_break_font_);
delete_link->SetController(this);
gfx::Rect delete_bounds = CalculateDeleteControlBounds(y);
delete_link->SetBounds(delete_bounds.x(), delete_bounds.y(),
delete_bounds.width(), delete_bounds.height());
floating_view = delete_link;
} else {
HistoryItemRenderer* renderer =
new HistoryItemRenderer(this,
show_results_);
renderer->SetModel(model_.get(), model_index);
renderer->SetBounds(kLeftMargin, y,
width() - kLeftMargin - kRightMargin,
GetEntryHeight());
floating_view = renderer;
}
floating_view->Layout();
AttachFloatingView(floating_view, id);
#ifdef DEBUG_FLOATING_VIEWS
floating_view->SetBackground(views::Background::CreateSolidBackground(
SkColorSetRGB(255, 0, 0)));
floating_view->SchedulePaint();
#endif
return floating_view;
}
int HistoryView::GetMaxViewID() {
if (!show_delete_controls_)
return model_->GetItemCount();
// Figure out how many delete controls we are displaying.
int deletes = 0;
for (BreakOffsets::iterator i = break_offsets_.begin();
i != break_offsets_.end(); ++i) {
if (i->second.day)
deletes++;
}
// Subtract one because we don't display a delete control at the end.
deletes--;
return std::max(0, deletes + model_->GetItemCount());
}
void HistoryView::LinkActivated(views::Link* source, int event_flags) {
DeleteDayAtModelIndex(source->GetID());
}
void HistoryView::DeleteDayAtModelIndex(int index) {
std::wstring text = l10n_util::GetString(
IDS_HISTORY_DELETE_PRIOR_VISITS_WARNING);
std::wstring caption = l10n_util::GetString(
IDS_HISTORY_DELETE_PRIOR_VISITS_WARNING_TITLE);
UINT flags = MB_OKCANCEL | MB_ICONWARNING | MB_TOPMOST | MB_SETFOREGROUND;
if (win_util::MessageBox(GetWidget()->GetHWND(),
text, caption, flags) != IDOK) {
return;
}
if (index < 0 || index >= model_->GetItemCount()) {
// Bogus index.
NOTREACHED();
return;
}
UserMetrics::RecordAction(L"History_DeleteHistory", model_->profile());
// BrowsingDataRemover deletes itself when done.
// index refers to the last page at the very end of the day, so we delete
// everything after the start of the day and before the end of the day.
Time delete_begin = model_->GetVisitTime(index).LocalMidnight();
Time delete_end =
(model_->GetVisitTime(index) + TimeDelta::FromDays(1)).LocalMidnight();
BrowsingDataRemover* remover =
new BrowsingDataRemover(model_->profile(),
delete_begin,
delete_end);
remover->Remove(BrowsingDataRemover::REMOVE_HISTORY |
BrowsingDataRemover::REMOVE_COOKIES |
BrowsingDataRemover::REMOVE_CACHE);
model_->Refresh();
// Scroll to the origin, otherwise the scroll position isn't changed and the
// user is left looking at a region they originally weren't viewing.
ScrollRectToVisible(0, 0, 0, 0);
}
int HistoryView::CalculateDeleteOffset(
const BreakOffsets::const_iterator& it) {
DCHECK(show_delete_controls_);
int offset = 0;
for (BreakOffsets::iterator i = break_offsets_.begin(); i != it; ++i) {
if (i->second.day)
offset++;
}
return offset;
}
int HistoryView::GetDeleteControlWidth() {
if (delete_control_width_)
return delete_control_width_;
EnsureRenderer();
gfx::Size pref = delete_renderer_->GetPreferredSize();
delete_control_width_ = pref.width();
return delete_control_width_;
}
gfx::Rect HistoryView::CalculateDeleteControlBounds(int base_y) {
// NOTE: the height here is too big, it should be just big enough to show
// the link. Additionally this should be baseline aligned with the date. I'm
// not doing that now as a redesign of HistoryView is in the works.
const int delete_width = GetDeleteControlWidth();
const int delete_x = width() - kRightMargin - delete_width;
return gfx::Rect(delete_x,
base_y + kDeleteControlOffset,
delete_width,
kBrowseResultsHeight);
}