Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1119)

Side by Side Diff: chrome/browser/ui/ash/launcher/chrome_launcher_controller_impl.cc

Issue 2791463002: mash: Remove ShelfDelegate; move functions to ShelfModel. (Closed)
Patch Set: Address comment. Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller_impl.h" 5 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller_impl.h"
6 6
7 #include <stddef.h> 7 #include <stddef.h>
8 8
9 #include <vector> 9 #include <vector>
10 10
(...skipping 235 matching lines...) Expand 10 before | Expand all | Expand 10 after
246 new MultiProfileAppWindowLauncherController(this)); 246 new MultiProfileAppWindowLauncherController(this));
247 } else { 247 } else {
248 // Create our v1/v2 application / browser monitors which will inform the 248 // Create our v1/v2 application / browser monitors which will inform the
249 // launcher of status changes. 249 // launcher of status changes.
250 browser_status_monitor_.reset(new BrowserStatusMonitor(this)); 250 browser_status_monitor_.reset(new BrowserStatusMonitor(this));
251 browser_status_monitor_->Initialize(); 251 browser_status_monitor_->Initialize();
252 extension_app_window_controller.reset( 252 extension_app_window_controller.reset(
253 new ExtensionAppWindowLauncherController(this)); 253 new ExtensionAppWindowLauncherController(this));
254 } 254 }
255 app_window_controllers_.push_back(std::move(extension_app_window_controller)); 255 app_window_controllers_.push_back(std::move(extension_app_window_controller));
256 256 app_window_controllers_.push_back(
257 std::unique_ptr<AppWindowLauncherController> arc_app_window_controller; 257 base::MakeUnique<ArcAppWindowLauncherController>(this));
258 arc_app_window_controller.reset(
259 new ArcAppWindowLauncherController(this, this));
260 app_window_controllers_.push_back(std::move(arc_app_window_controller));
261 258
262 // Right now ash::Shell isn't created for tests. 259 // Right now ash::Shell isn't created for tests.
263 // TODO(mukai): Allows it to observe display change and write tests. 260 // TODO(mukai): Allows it to observe display change and write tests.
264 if (ash::Shell::HasInstance()) 261 if (ash::Shell::HasInstance())
265 ash::Shell::Get()->window_tree_host_manager()->AddObserver(this); 262 ash::Shell::Get()->window_tree_host_manager()->AddObserver(this);
266 } 263 }
267 264
268 ChromeLauncherControllerImpl::~ChromeLauncherControllerImpl() { 265 ChromeLauncherControllerImpl::~ChromeLauncherControllerImpl() {
269 // Reset the BrowserStatusMonitor as it has a weak pointer to this. 266 // Reset the BrowserStatusMonitor as it has a weak pointer to this.
270 browser_status_monitor_.reset(); 267 browser_status_monitor_.reset();
(...skipping 469 matching lines...) Expand 10 before | Expand all | Expand 10 after
740 } 737 }
741 738
742 app_list::AppListSyncableService* app_service = 739 app_list::AppListSyncableService* app_service =
743 app_list::AppListSyncableServiceFactory::GetForProfile(profile()); 740 app_list::AppListSyncableServiceFactory::GetForProfile(profile());
744 if (app_service) 741 if (app_service)
745 app_service->AddObserverAndStart(this); 742 app_service->AddObserverAndStart(this);
746 743
747 PrefServiceSyncableFromProfile(profile())->AddObserver(this); 744 PrefServiceSyncableFromProfile(profile())->AddObserver(this);
748 } 745 }
749 746
750 ///////////////////////////////////////////////////////////////////////////////
751 // ash::ShelfDelegate:
752
753 ash::ShelfID ChromeLauncherControllerImpl::GetShelfIDForAppID( 747 ash::ShelfID ChromeLauncherControllerImpl::GetShelfIDForAppID(
754 const std::string& app_id) { 748 const std::string& app_id) {
755 // Get shelf id for |app_id| and an empty |launch_id|. 749 return model_->GetShelfIDForAppID(app_id);
756 return GetShelfIDForAppIDAndLaunchID(app_id, std::string());
757 } 750 }
758 751
759 ash::ShelfID ChromeLauncherControllerImpl::GetShelfIDForAppIDAndLaunchID( 752 ash::ShelfID ChromeLauncherControllerImpl::GetShelfIDForAppIDAndLaunchID(
760 const std::string& app_id, 753 const std::string& app_id,
761 const std::string& launch_id) { 754 const std::string& launch_id) {
762 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859 755 return model_->GetShelfIDForAppIDAndLaunchID(app_id, launch_id);
763 const std::string shelf_app_id =
764 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id);
765 if (shelf_app_id.empty())
766 return ash::kInvalidShelfID;
767
768 for (const ash::ShelfItem& item : model_->items()) {
769 // Ash's ShelfWindowWatcher handles app panel windows separately.
770 if (item.type != ash::TYPE_APP_PANEL &&
771 item.app_launch_id.app_id() == shelf_app_id &&
772 item.app_launch_id.launch_id() == launch_id) {
773 return item.id;
774 }
775 }
776 return ash::kInvalidShelfID;
777 } 756 }
778 757
779 const std::string& ChromeLauncherControllerImpl::GetAppIDForShelfID( 758 const std::string& ChromeLauncherControllerImpl::GetAppIDForShelfID(
780 ash::ShelfID id) { 759 ash::ShelfID id) {
781 ash::ShelfItems::const_iterator item = model_->ItemByID(id); 760 return model_->GetAppIDForShelfID(id);
782 return item != model_->items().end() ? item->app_launch_id.app_id()
783 : base::EmptyString();
784 } 761 }
785 762
786 void ChromeLauncherControllerImpl::PinAppWithID(const std::string& app_id) { 763 void ChromeLauncherControllerImpl::PinAppWithID(const std::string& app_id) {
787 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859 764 model_->PinAppWithID(app_id);
788 const std::string shelf_app_id =
789 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id);
790
791 // Requests to pin should only be be made for apps with editable pin states.
792 DCHECK_EQ(GetPinnableForAppID(shelf_app_id, profile()),
793 AppListControllerDelegate::PIN_EDITABLE);
794
795 // If the app is already pinned, do nothing and return.
796 if (IsAppPinned(shelf_app_id))
797 return;
798
799 // Convert an existing item to be pinned, or create a new pinned item.
800 ash::ShelfID shelf_id = GetShelfIDForAppID(shelf_app_id);
801 if (shelf_id != ash::kInvalidShelfID) {
802 DCHECK_EQ(GetItem(shelf_id)->type, ash::TYPE_APP);
803 DCHECK(!GetItem(shelf_id)->pinned_by_policy);
804 SetItemType(shelf_id, ash::TYPE_PINNED_APP);
805 } else {
806 shelf_id = CreateAppShortcutLauncherItem(ash::AppLaunchId(shelf_app_id),
807 model_->item_count());
808 }
809 } 765 }
810 766
811 bool ChromeLauncherControllerImpl::IsAppPinned(const std::string& app_id) { 767 bool ChromeLauncherControllerImpl::IsAppPinned(const std::string& app_id) {
812 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859 768 return model_->IsAppPinned(app_id);
813 const std::string shelf_app_id =
814 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id);
815
816 return IsPinned(GetShelfIDForAppID(shelf_app_id));
817 } 769 }
818 770
819 void ChromeLauncherControllerImpl::UnpinAppWithID(const std::string& app_id) { 771 void ChromeLauncherControllerImpl::UnpinAppWithID(const std::string& app_id) {
820 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859 772 model_->UnpinAppWithID(app_id);
821 const std::string shelf_app_id =
822 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id);
823
824 // Requests to unpin should only be be made for apps with editable pin states.
825 DCHECK_EQ(GetPinnableForAppID(shelf_app_id, profile()),
826 AppListControllerDelegate::PIN_EDITABLE);
827
828 // If the app is pinned, unpin the shelf item (and remove it if not running).
829 if (IsAppPinned(shelf_app_id))
830 UnpinShelfItemInternal(GetShelfIDForAppID(shelf_app_id));
831 } 773 }
832 774
833 /////////////////////////////////////////////////////////////////////////////// 775 ///////////////////////////////////////////////////////////////////////////////
834 // LauncherAppUpdater::Delegate: 776 // LauncherAppUpdater::Delegate:
835 777
836 void ChromeLauncherControllerImpl::OnAppInstalled( 778 void ChromeLauncherControllerImpl::OnAppInstalled(
837 content::BrowserContext* browser_context, 779 content::BrowserContext* browser_context,
838 const std::string& app_id) { 780 const std::string& app_id) {
839 if (IsAppPinned(app_id)) { 781 if (IsAppPinned(app_id)) {
840 // Clear and re-fetch to ensure icon is up-to-date. 782 // Clear and re-fetch to ensure icon is up-to-date.
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after
931 int app_index = model_->ItemIndexByID(item->id); 873 int app_index = model_->ItemIndexByID(item->id);
932 DCHECK_GE(app_index, 0); 874 DCHECK_GE(app_index, 0);
933 if (running_index != app_index) 875 if (running_index != app_index)
934 model_->Move(running_index, app_index); 876 model_->Move(running_index, app_index);
935 running_index++; 877 running_index++;
936 } 878 }
937 } 879 }
938 } 880 }
939 881
940 void ChromeLauncherControllerImpl::RemoveShelfItem(ash::ShelfID id) { 882 void ChromeLauncherControllerImpl::RemoveShelfItem(ash::ShelfID id) {
941 const std::string& app_id = GetAppIDForShelfID(id);
942 AppIconLoader* app_icon_loader = GetAppIconLoaderForApp(app_id);
943 if (app_icon_loader)
944 app_icon_loader->ClearImage(app_id);
945 const int index = model_->ItemIndexByID(id); 883 const int index = model_->ItemIndexByID(id);
946 // A "browser proxy" is not known to the model and this removal does 884 if (index >= 0 && index < model_->item_count())
947 // therefore not need to be propagated to the model.
948 if (index != -1)
949 model_->RemoveItemAt(index); 885 model_->RemoveItemAt(index);
950 } 886 }
951 887
952 void ChromeLauncherControllerImpl::PinRunningAppInternal( 888 void ChromeLauncherControllerImpl::PinRunningAppInternal(
953 int index, 889 int index,
954 ash::ShelfID shelf_id) { 890 ash::ShelfID shelf_id) {
955 DCHECK_EQ(GetItem(shelf_id)->type, ash::TYPE_APP); 891 DCHECK_EQ(GetItem(shelf_id)->type, ash::TYPE_APP);
956 SetItemType(shelf_id, ash::TYPE_PINNED_APP); 892 SetItemType(shelf_id, ash::TYPE_PINNED_APP);
957 int running_index = model_->ItemIndexByID(shelf_id); 893 int running_index = model_->ItemIndexByID(shelf_id);
958 if (running_index < index) 894 if (running_index < index)
(...skipping 197 matching lines...) Expand 10 before | Expand all | Expand 10 after
1156 ash::ShelfID ChromeLauncherControllerImpl::InsertAppLauncherItem( 1092 ash::ShelfID ChromeLauncherControllerImpl::InsertAppLauncherItem(
1157 std::unique_ptr<ash::ShelfItemDelegate> item_delegate, 1093 std::unique_ptr<ash::ShelfItemDelegate> item_delegate,
1158 ash::ShelfItemStatus status, 1094 ash::ShelfItemStatus status,
1159 int index, 1095 int index,
1160 ash::ShelfItemType shelf_item_type) { 1096 ash::ShelfItemType shelf_item_type) {
1161 ash::ShelfID id = model_->next_id(); 1097 ash::ShelfID id = model_->next_id();
1162 CHECK(!GetItem(id)); 1098 CHECK(!GetItem(id));
1163 CHECK(item_delegate); 1099 CHECK(item_delegate);
1164 // Ash's ShelfWindowWatcher handles app panel windows separately. 1100 // Ash's ShelfWindowWatcher handles app panel windows separately.
1165 DCHECK_NE(ash::TYPE_APP_PANEL, shelf_item_type); 1101 DCHECK_NE(ash::TYPE_APP_PANEL, shelf_item_type);
1166
1167 ash::ShelfItem item; 1102 ash::ShelfItem item;
1103 item.status = status;
1168 item.type = shelf_item_type; 1104 item.type = shelf_item_type;
1169 item.app_launch_id = item_delegate->app_launch_id(); 1105 item.app_launch_id = item_delegate->app_launch_id();
1170 item.image = extensions::util::GetDefaultAppIcon(); 1106 // Set the delegate first to avoid constructing one in ShelfItemAdded.
1171 1107 model_->SetShelfItemDelegate(id, std::move(item_delegate));
1172 const std::string& app_id = item_delegate->app_id();
1173 item.title = LauncherControllerHelper::GetAppTitle(profile(), app_id);
1174
1175 ash::ShelfItemStatus new_state = GetAppState(app_id);
1176 if (new_state != ash::STATUS_CLOSED)
1177 status = new_state;
1178
1179 item.status = status;
1180 model_->AddAt(index, item); 1108 model_->AddAt(index, item);
1181
1182 AppIconLoader* app_icon_loader = GetAppIconLoaderForApp(app_id);
1183 if (app_icon_loader) {
1184 app_icon_loader->FetchImage(app_id);
1185 app_icon_loader->UpdateImage(app_id);
1186 }
1187
1188 model_->SetShelfItemDelegate(id, std::move(item_delegate));
1189 return id; 1109 return id;
1190 } 1110 }
1191 1111
1192 void ChromeLauncherControllerImpl::CreateBrowserShortcutLauncherItem() { 1112 void ChromeLauncherControllerImpl::CreateBrowserShortcutLauncherItem() {
1193 // Do not sync the pin position of the browser shortcut item when it is added; 1113 // Do not sync the pin position of the browser shortcut item when it is added;
1194 // its initial position before prefs have loaded is unimportant and the sync 1114 // its initial position before prefs have loaded is unimportant and the sync
1195 // service may not yet be initialized. 1115 // service may not yet be initialized.
1196 ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler(); 1116 ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler();
1197 1117
1198 ash::ShelfItem browser_shortcut; 1118 ash::ShelfItem browser_shortcut;
1199 browser_shortcut.type = ash::TYPE_BROWSER_SHORTCUT; 1119 browser_shortcut.type = ash::TYPE_BROWSER_SHORTCUT;
1200 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 1120 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
1201 browser_shortcut.image = *rb.GetImageSkiaNamed(IDR_PRODUCT_LOGO_32); 1121 browser_shortcut.image = *rb.GetImageSkiaNamed(IDR_PRODUCT_LOGO_32);
1202 browser_shortcut.title = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); 1122 browser_shortcut.title = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME);
1203 browser_shortcut.app_launch_id = ash::AppLaunchId(kChromeAppId); 1123 browser_shortcut.app_launch_id = ash::AppLaunchId(kChromeAppId);
1204 ash::ShelfID id = model_->next_id(); 1124 ash::ShelfID id = model_->next_id();
1205 model_->AddAt(0, browser_shortcut);
1206 std::unique_ptr<BrowserShortcutLauncherItemController> item_delegate = 1125 std::unique_ptr<BrowserShortcutLauncherItemController> item_delegate =
1207 base::MakeUnique<BrowserShortcutLauncherItemController>(model_); 1126 base::MakeUnique<BrowserShortcutLauncherItemController>(model_);
1208 BrowserShortcutLauncherItemController* item_controller = item_delegate.get(); 1127 BrowserShortcutLauncherItemController* item_controller = item_delegate.get();
1128 // Set the delegate first to avoid constructing another one in ShelfItemAdded.
1209 model_->SetShelfItemDelegate(id, std::move(item_delegate)); 1129 model_->SetShelfItemDelegate(id, std::move(item_delegate));
1130 model_->AddAt(0, browser_shortcut);
1210 item_controller->UpdateBrowserItemState(); 1131 item_controller->UpdateBrowserItemState();
1211 } 1132 }
1212 1133
1213 bool ChromeLauncherControllerImpl::IsIncognito( 1134 bool ChromeLauncherControllerImpl::IsIncognito(
1214 const content::WebContents* web_contents) const { 1135 const content::WebContents* web_contents) const {
1215 const Profile* profile = 1136 const Profile* profile =
1216 Profile::FromBrowserContext(web_contents->GetBrowserContext()); 1137 Profile::FromBrowserContext(web_contents->GetBrowserContext());
1217 return profile->IsOffTheRecord() && !profile->IsGuestSession() && 1138 return profile->IsOffTheRecord() && !profile->IsGuestSession() &&
1218 !profile->IsSystemProfile(); 1139 !profile->IsSystemProfile();
1219 } 1140 }
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
1269 app_service->RemoveObserver(this); 1190 app_service->RemoveObserver(this);
1270 1191
1271 PrefServiceSyncableFromProfile(profile())->RemoveObserver(this); 1192 PrefServiceSyncableFromProfile(profile())->RemoveObserver(this);
1272 } 1193 }
1273 1194
1274 /////////////////////////////////////////////////////////////////////////////// 1195 ///////////////////////////////////////////////////////////////////////////////
1275 // ash::ShelfModelObserver: 1196 // ash::ShelfModelObserver:
1276 1197
1277 void ChromeLauncherControllerImpl::ShelfItemAdded(int index) { 1198 void ChromeLauncherControllerImpl::ShelfItemAdded(int index) {
1278 // Update the pin position preference as needed. 1199 // Update the pin position preference as needed.
1279 const ash::ShelfItem& item = model_->items()[index]; 1200 ash::ShelfItem item = model_->items()[index];
1280 if (ItemTypeIsPinned(item) && should_sync_pin_changes()) 1201 if (ItemTypeIsPinned(item) && should_sync_pin_changes())
1281 SyncPinPosition(item.id); 1202 SyncPinPosition(item.id);
1203
1204 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859
1205 const std::string shelf_app_id =
1206 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(
1207 item.app_launch_id.app_id());
1208
1209 // Fetch and update the icon for the app's item.
1210 AppIconLoader* app_icon_loader = GetAppIconLoaderForApp(shelf_app_id);
1211 if (app_icon_loader) {
1212 app_icon_loader->FetchImage(shelf_app_id);
1213 app_icon_loader->UpdateImage(shelf_app_id);
1214 }
1215
1216 // Update the item with any missing Chrome-specific info.
1217 if (item.type == ash::TYPE_APP || item.type == ash::TYPE_PINNED_APP) {
1218 bool needs_update = false;
1219 if (item.image.isNull()) {
1220 needs_update = true;
1221 item.image = extensions::util::GetDefaultAppIcon();
1222 }
1223 if (item.title.empty()) {
1224 needs_update = true;
1225 item.title =
1226 LauncherControllerHelper::GetAppTitle(profile(), shelf_app_id);
1227 }
1228 ash::ShelfItemStatus status = GetAppState(shelf_app_id);
1229 if (status != item.status && status != ash::STATUS_CLOSED) {
1230 needs_update = true;
1231 item.status = status;
1232 }
1233 if (needs_update)
1234 model_->Set(index, item);
1235 }
1236
1237 // Construct a ShelfItemDelegate for the item if one does not yet exist.
1238 if (!model_->GetShelfItemDelegate(item.id)) {
1239 model_->SetShelfItemDelegate(
1240 item.id, AppShortcutLauncherItemController::Create(ash::AppLaunchId(
1241 shelf_app_id, item.app_launch_id.launch_id())));
1242 }
1282 } 1243 }
1283 1244
1284 void ChromeLauncherControllerImpl::ShelfItemRemoved( 1245 void ChromeLauncherControllerImpl::ShelfItemRemoved(
1285 int index, 1246 int index,
1286 const ash::ShelfItem& old_item) { 1247 const ash::ShelfItem& old_item) {
1248 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859
1249 const std::string shelf_app_id =
1250 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(
1251 old_item.app_launch_id.app_id());
1252
1287 // Remove the pin position from preferences as needed. 1253 // Remove the pin position from preferences as needed.
1288 if (ItemTypeIsPinned(old_item) && should_sync_pin_changes()) { 1254 if (ItemTypeIsPinned(old_item) && should_sync_pin_changes()) {
1289 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859
1290 const std::string shelf_app_id =
1291 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(
1292 old_item.app_launch_id.app_id());
1293 ash::AppLaunchId app_launch_id(shelf_app_id, 1255 ash::AppLaunchId app_launch_id(shelf_app_id,
1294 old_item.app_launch_id.launch_id()); 1256 old_item.app_launch_id.launch_id());
1295 ash::launcher::RemovePinPosition(profile(), app_launch_id); 1257 ash::launcher::RemovePinPosition(profile(), app_launch_id);
1296 } 1258 }
1259
1260 AppIconLoader* app_icon_loader = GetAppIconLoaderForApp(shelf_app_id);
1261 if (app_icon_loader)
1262 app_icon_loader->ClearImage(shelf_app_id);
1297 } 1263 }
1298 1264
1299 void ChromeLauncherControllerImpl::ShelfItemMoved(int start_index, 1265 void ChromeLauncherControllerImpl::ShelfItemMoved(int start_index,
1300 int target_index) { 1266 int target_index) {
1301 // Update the pin position preference as needed. 1267 // Update the pin position preference as needed.
1302 const ash::ShelfItem& item = model_->items()[target_index]; 1268 const ash::ShelfItem& item = model_->items()[target_index];
1303 DCHECK_NE(ash::TYPE_APP_LIST, item.type); 1269 DCHECK_NE(ash::TYPE_APP_LIST, item.type);
1304 if (ItemTypeIsPinned(item) && should_sync_pin_changes()) 1270 if (ItemTypeIsPinned(item) && should_sync_pin_changes())
1305 SyncPinPosition(item.id); 1271 SyncPinPosition(item.id);
1306 } 1272 }
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
1374 item.app_launch_id.app_id() != app_id) { 1340 item.app_launch_id.app_id() != app_id) {
1375 continue; 1341 continue;
1376 } 1342 }
1377 item.image = image; 1343 item.image = image;
1378 if (arc_deferred_launcher_) 1344 if (arc_deferred_launcher_)
1379 arc_deferred_launcher_->MaybeApplySpinningEffect(app_id, &item.image); 1345 arc_deferred_launcher_->MaybeApplySpinningEffect(app_id, &item.image);
1380 model_->Set(index, item); 1346 model_->Set(index, item);
1381 // It's possible we're waiting on more than one item, so don't break. 1347 // It's possible we're waiting on more than one item, so don't break.
1382 } 1348 }
1383 } 1349 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698