1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/webui/settings/custom_home_pages_table_model.h"
6
7 #include <stddef.h>
8
9 #include "base/bind.h"
10 #include "base/callback_helpers.h"
11 #include "base/i18n/rtl.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/history/history_service_factory.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/browser_list.h"
17 #include "chrome/browser/ui/tabs/tab_strip_model.h"
18 #include "chrome/common/url_constants.h"
19 #include "chrome/grit/generated_resources.h"
20 #include "components/history/core/browser/history_service.h"
21 #include "components/url_formatter/url_formatter.h"
22 #include "content/public/browser/web_contents.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/models/table_model_observer.h"
25 #include "ui/gfx/codec/png_codec.h"
26 #include "url/gurl.h"
27
28 #if defined(OS_CHROMEOS)
29 #include "chrome/browser/ui/settings_window_manager_chromeos.h"
30 #endif
31
32 struct CustomHomePagesTableModel::Entry {
EntryCustomHomePagesTableModel::Entry33 Entry() : task_id(base::CancelableTaskTracker::kBadTaskId) {}
34
35 // URL of the page.
36 GURL url;
37
38 // Page title. If this is empty, we'll display the URL as the entry.
39 base::string16 title;
40
41 // If not |base::CancelableTaskTracker::kBadTaskId|, indicates we're loading
42 // the title for the page.
43 base::CancelableTaskTracker::TaskId task_id;
44 };
45
CustomHomePagesTableModel(Profile * profile)46 CustomHomePagesTableModel::CustomHomePagesTableModel(Profile* profile)
47 : profile_(profile),
48 observer_(nullptr),
49 num_outstanding_title_lookups_(0) {}
50
~CustomHomePagesTableModel()51 CustomHomePagesTableModel::~CustomHomePagesTableModel() {
52 }
53
SetURLs(const std::vector<GURL> & urls)54 void CustomHomePagesTableModel::SetURLs(const std::vector<GURL>& urls) {
55 entries_.resize(urls.size());
56 for (size_t i = 0; i < urls.size(); ++i) {
57 entries_[i].url = urls[i];
58 entries_[i].title.erase();
59 }
60 LoadAllTitles();
61 }
62
63 /**
64 * Move a number of existing entries to a new position, reordering the table.
65 *
66 * We determine the range of elements affected by the move, save the moved
67 * elements, compact the remaining ones, and re-insert moved elements.
68 * Expects |index_list| to be ordered ascending.
69 */
MoveURLs(int insert_before,const std::vector<int> & index_list)70 void CustomHomePagesTableModel::MoveURLs(int insert_before,
71 const std::vector<int>& index_list) {
72 if (index_list.empty()) return;
73 DCHECK(insert_before >= 0 && insert_before <= RowCount());
74
75 // The range of elements that needs to be reshuffled is [ |first|, |last| ).
76 int first = std::min(insert_before, index_list.front());
77 int last = std::max(insert_before, index_list.back() + 1);
78
79 // Save the dragged elements. Also, adjust insertion point if it is before a
80 // dragged element.
81 std::vector<Entry> moved_entries;
82 for (size_t i = 0; i < index_list.size(); ++i) {
83 moved_entries.push_back(entries_[index_list[i]]);
84 if (index_list[i] == insert_before)
85 insert_before++;
86 }
87
88 // Compact the range between beginning and insertion point, moving downwards.
89 size_t skip_count = 0;
90 for (int i = first; i < insert_before; ++i) {
91 if (skip_count < index_list.size() && index_list[skip_count] == i)
92 skip_count++;
93 else
94 entries_[i - skip_count] = entries_[i];
95 }
96
97 // Moving items down created a gap. We start compacting up after it.
98 first = insert_before;
99 insert_before -= skip_count;
100
101 // Now compact up for elements after the insertion point.
102 skip_count = 0;
103 for (int i = last - 1; i >= first; --i) {
104 if (skip_count < index_list.size() &&
105 index_list[index_list.size() - skip_count - 1] == i) {
106 skip_count++;
107 } else {
108 entries_[i + skip_count] = entries_[i];
109 }
110 }
111
112 // Insert moved elements.
113 std::copy(moved_entries.begin(), moved_entries.end(),
114 entries_.begin() + insert_before);
115
116 // Possibly large change, so tell the view to just rebuild itself.
117 if (observer_)
118 observer_->OnModelChanged();
119 }
120
AddWithoutNotification(int index,const GURL & url)121 void CustomHomePagesTableModel::AddWithoutNotification(
122 int index, const GURL& url) {
123 DCHECK(index >= 0 && index <= RowCount());
124 entries_.insert(entries_.begin() + static_cast<size_t>(index), Entry());
125 entries_[index].url = url;
126 }
127
Add(int index,const GURL & url)128 void CustomHomePagesTableModel::Add(int index, const GURL& url) {
129 AddWithoutNotification(index, url);
130 LoadTitle(&(entries_[index]));
131 if (observer_)
132 observer_->OnItemsAdded(index, 1);
133 }
134
RemoveWithoutNotification(int index)135 void CustomHomePagesTableModel::RemoveWithoutNotification(int index) {
136 DCHECK(index >= 0 && index < RowCount());
137 Entry* entry = &(entries_[index]);
138 // Cancel any pending load requests now so we don't deref a bogus pointer when
139 // we get the loaded notification.
140 if (entry->task_id != base::CancelableTaskTracker::kBadTaskId) {
141 task_tracker_.TryCancel(entry->task_id);
142 entry->task_id = base::CancelableTaskTracker::kBadTaskId;
143 }
144 entries_.erase(entries_.begin() + static_cast<size_t>(index));
145 }
146
Remove(int index)147 void CustomHomePagesTableModel::Remove(int index) {
148 RemoveWithoutNotification(index);
149 if (observer_)
150 observer_->OnItemsRemoved(index, 1);
151 }
152
SetToCurrentlyOpenPages(content::WebContents * ignore_contents)153 void CustomHomePagesTableModel::SetToCurrentlyOpenPages(
154 content::WebContents* ignore_contents) {
155 // Remove the current entries.
156 while (RowCount())
157 RemoveWithoutNotification(0);
158
159 // Add tabs from appropriate browser windows.
160 int add_index = 0;
161 for (auto* browser : *BrowserList::GetInstance()) {
162 if (!ShouldIncludeBrowser(browser))
163 continue;
164
165 for (int tab_index = 0;
166 tab_index < browser->tab_strip_model()->count();
167 ++tab_index) {
168 content::WebContents* contents =
169 browser->tab_strip_model()->GetWebContentsAt(tab_index);
170 if (contents == ignore_contents)
171 continue;
172 const GURL url = contents->GetURL();
173 if (!url.is_empty() && !url.SchemeIs(content::kChromeDevToolsScheme))
174 AddWithoutNotification(add_index++, url);
175 }
176 }
177 LoadAllTitles();
178 }
179
GetURLs()180 std::vector<GURL> CustomHomePagesTableModel::GetURLs() {
181 std::vector<GURL> urls(entries_.size());
182 for (size_t i = 0; i < entries_.size(); ++i)
183 urls[i] = entries_[i].url;
184 return urls;
185 }
186
RowCount()187 int CustomHomePagesTableModel::RowCount() {
188 return static_cast<int>(entries_.size());
189 }
190
GetText(int row,int column_id)191 base::string16 CustomHomePagesTableModel::GetText(int row, int column_id) {
192 DCHECK(column_id == 0);
193 DCHECK(row >= 0 && row < RowCount());
194 return entries_[row].title.empty() ? FormattedURL(row) : entries_[row].title;
195 }
196
GetTooltip(int row)197 base::string16 CustomHomePagesTableModel::GetTooltip(int row) {
198 return entries_[row].title.empty()
199 ? base::string16()
200 : l10n_util::GetStringFUTF16(IDS_SETTINGS_ON_STARTUP_PAGE_TOOLTIP,
201 entries_[row].title,
202 FormattedURL(row));
203 }
204
SetObserver(ui::TableModelObserver * observer)205 void CustomHomePagesTableModel::SetObserver(ui::TableModelObserver* observer) {
206 observer_ = observer;
207 }
208
ShouldIncludeBrowser(Browser * browser)209 bool CustomHomePagesTableModel::ShouldIncludeBrowser(Browser* browser) {
210 // Do not include incognito browsers.
211 if (browser->profile() != profile_)
212 return false;
213 #if defined(OS_CHROMEOS)
214 // Do not include the Settings window.
215 if (chrome::SettingsWindowManager::GetInstance()->IsSettingsBrowser(
216 browser)) {
217 return false;
218 }
219 #endif
220 return true;
221 }
222
LoadTitle(Entry * entry)223 void CustomHomePagesTableModel::LoadTitle(Entry* entry) {
224 history::HistoryService* history_service =
225 HistoryServiceFactory::GetForProfile(profile_,
226 ServiceAccessType::EXPLICIT_ACCESS);
227 if (history_service) {
228 entry->task_id = history_service->QueryURL(
229 entry->url, false,
230 base::BindOnce(&CustomHomePagesTableModel::OnGotTitle,
231 base::Unretained(this), entry->url, false),
232 &task_tracker_);
233 }
234 }
235
LoadAllTitles()236 void CustomHomePagesTableModel::LoadAllTitles() {
237 history::HistoryService* history_service =
238 HistoryServiceFactory::GetForProfile(profile_,
239 ServiceAccessType::EXPLICIT_ACCESS);
240 // It's possible for multiple LoadAllTitles() queries to be inflight we want
241 // to make sure everything is resolved before updating the observer or we risk
242 // getting rendering glitches.
243 num_outstanding_title_lookups_ += entries_.size();
244 for (Entry& entry : entries_) {
245 if (history_service) {
246 entry.task_id = history_service->QueryURL(
247 entry.url, false,
248 base::BindOnce(&CustomHomePagesTableModel::OnGotOneOfManyTitles,
249 base::Unretained(this), entry.url),
250 &task_tracker_);
251 }
252 }
253 if (entries_.empty())
254 observer_->OnModelChanged();
255 }
256
OnGotOneOfManyTitles(const GURL & entry_url,history::QueryURLResult result)257 void CustomHomePagesTableModel::OnGotOneOfManyTitles(
258 const GURL& entry_url,
259 history::QueryURLResult result) {
260 OnGotTitle(entry_url, false, std::move(result));
261 DCHECK_GE(num_outstanding_title_lookups_, 1);
262 if (--num_outstanding_title_lookups_ == 0 && observer_)
263 observer_->OnModelChanged();
264 }
265
OnGotTitle(const GURL & entry_url,bool observable,history::QueryURLResult result)266 void CustomHomePagesTableModel::OnGotTitle(const GURL& entry_url,
267 bool observable,
268 history::QueryURLResult result) {
269 Entry* entry = NULL;
270 size_t entry_index = 0;
271 for (size_t i = 0; i < entries_.size(); ++i) {
272 if (entries_[i].url == entry_url) {
273 entry = &entries_[i];
274 entry_index = i;
275 break;
276 }
277 }
278 if (!entry) {
279 // The URLs changed before we were called back.
280 return;
281 }
282 entry->task_id = base::CancelableTaskTracker::kBadTaskId;
283 if (result.success && !result.row.title().empty()) {
284 entry->title = result.row.title();
285 if (observer_ && observable)
286 observer_->OnItemsChanged(static_cast<int>(entry_index), 1);
287 }
288 }
289
FormattedURL(int row) const290 base::string16 CustomHomePagesTableModel::FormattedURL(int row) const {
291 base::string16 url = url_formatter::FormatUrl(entries_[row].url);
292 url = base::i18n::GetDisplayStringInLTRDirectionality(url);
293 return url;
294 }
295