| // 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); |
| } |