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

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

Issue 2769323002: mash: Update shelf pin prefs in ShelfModelObserver overrides. (Closed)
Patch Set: Use ScopedPinSyncDisabler name suggestion. 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 94 matching lines...) Expand 10 before | Expand all | Expand 10 after
105 // A callback that does nothing after shelf item selection handling. 105 // A callback that does nothing after shelf item selection handling.
106 void NoopCallback(ash::ShelfAction, base::Optional<MenuItemList>) {} 106 void NoopCallback(ash::ShelfAction, base::Optional<MenuItemList>) {}
107 107
108 // Calls ItemSelected with |source|, default arguments, and no callback. 108 // Calls ItemSelected with |source|, default arguments, and no callback.
109 void SelectItemWithSource(ash::mojom::ShelfItemDelegate* delegate, 109 void SelectItemWithSource(ash::mojom::ShelfItemDelegate* delegate,
110 ash::ShelfLaunchSource source) { 110 ash::ShelfLaunchSource source) {
111 delegate->ItemSelected(nullptr, display::kInvalidDisplayId, source, 111 delegate->ItemSelected(nullptr, display::kInvalidDisplayId, source,
112 base::Bind(&NoopCallback)); 112 base::Bind(&NoopCallback));
113 } 113 }
114 114
115 // Returns true if the given |item| has a pinned shelf item type.
116 bool ItemTypeIsPinned(const ash::ShelfItem& item) {
117 return item.type == ash::TYPE_PINNED_APP ||
118 item.type == ash::TYPE_BROWSER_SHORTCUT;
119 }
120
115 } // namespace 121 } // namespace
116 122
117 // A class to get events from ChromeOS when a user gets changed or added. 123 // A class to get events from ChromeOS when a user gets changed or added.
118 class ChromeLauncherControllerUserSwitchObserver 124 class ChromeLauncherControllerUserSwitchObserver
119 : public user_manager::UserManager::UserSessionStateObserver { 125 : public user_manager::UserManager::UserSessionStateObserver {
120 public: 126 public:
121 ChromeLauncherControllerUserSwitchObserver( 127 ChromeLauncherControllerUserSwitchObserver(
122 ChromeLauncherControllerImpl* controller) 128 ChromeLauncherControllerImpl* controller)
123 : controller_(controller) { 129 : controller_(controller) {
124 DCHECK(user_manager::UserManager::IsInitialized()); 130 DCHECK(user_manager::UserManager::IsInitialized());
(...skipping 229 matching lines...) Expand 10 before | Expand all | Expand 10 after
354 const ash::ShelfItem* item = GetItem(id); 360 const ash::ShelfItem* item = GetItem(id);
355 LauncherItemController* controller = GetLauncherItemController(id); 361 LauncherItemController* controller = GetLauncherItemController(id);
356 if (item && (item->status != ash::STATUS_CLOSED || controller->locked())) 362 if (item && (item->status != ash::STATUS_CLOSED || controller->locked()))
357 UnpinRunningAppInternal(model_->ItemIndexByID(id)); 363 UnpinRunningAppInternal(model_->ItemIndexByID(id));
358 else 364 else
359 LauncherItemClosed(id); 365 LauncherItemClosed(id);
360 } 366 }
361 367
362 bool ChromeLauncherControllerImpl::IsPinned(ash::ShelfID id) { 368 bool ChromeLauncherControllerImpl::IsPinned(ash::ShelfID id) {
363 const ash::ShelfItem* item = GetItem(id); 369 const ash::ShelfItem* item = GetItem(id);
364 return item && (item->type == ash::TYPE_PINNED_APP || 370 return item && ItemTypeIsPinned(*item);
365 item->type == ash::TYPE_BROWSER_SHORTCUT);
366 } 371 }
367 372
368 void ChromeLauncherControllerImpl::LockV1AppWithID(const std::string& app_id) { 373 void ChromeLauncherControllerImpl::LockV1AppWithID(const std::string& app_id) {
369 ash::ShelfID id = GetShelfIDForAppID(app_id); 374 ash::ShelfID id = GetShelfIDForAppID(app_id);
370 if (id == ash::kInvalidShelfID) { 375 if (id == ash::kInvalidShelfID) {
371 CreateAppShortcutLauncherItemWithType(ash::AppLaunchId(app_id), 376 CreateAppShortcutLauncherItemWithType(ash::AppLaunchId(app_id),
372 model_->item_count(), ash::TYPE_APP); 377 model_->item_count(), ash::TYPE_APP);
373 id = GetShelfIDForAppID(app_id); 378 id = GetShelfIDForAppID(app_id);
374 } 379 }
375 CHECK(id); 380 CHECK(id);
(...skipping 467 matching lines...) Expand 10 before | Expand all | Expand 10 after
843 // Convert an existing item to be pinned, or create a new pinned item. 848 // Convert an existing item to be pinned, or create a new pinned item.
844 ash::ShelfID shelf_id = GetShelfIDForAppID(shelf_app_id); 849 ash::ShelfID shelf_id = GetShelfIDForAppID(shelf_app_id);
845 if (shelf_id != ash::kInvalidShelfID) { 850 if (shelf_id != ash::kInvalidShelfID) {
846 DCHECK_EQ(GetItem(shelf_id)->type, ash::TYPE_APP); 851 DCHECK_EQ(GetItem(shelf_id)->type, ash::TYPE_APP);
847 DCHECK(!GetItem(shelf_id)->pinned_by_policy); 852 DCHECK(!GetItem(shelf_id)->pinned_by_policy);
848 SetItemType(shelf_id, ash::TYPE_PINNED_APP); 853 SetItemType(shelf_id, ash::TYPE_PINNED_APP);
849 } else { 854 } else {
850 shelf_id = CreateAppShortcutLauncherItem(ash::AppLaunchId(shelf_app_id), 855 shelf_id = CreateAppShortcutLauncherItem(ash::AppLaunchId(shelf_app_id),
851 model_->item_count()); 856 model_->item_count());
852 } 857 }
853
854 // TODO(msw): Trigger pref updates in ShelfModelObserver overrides.
855 SyncPinPosition(shelf_id);
856 } 858 }
857 859
858 bool ChromeLauncherControllerImpl::IsAppPinned(const std::string& app_id) { 860 bool ChromeLauncherControllerImpl::IsAppPinned(const std::string& app_id) {
859 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859 861 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859
860 const std::string shelf_app_id = 862 const std::string shelf_app_id =
861 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id); 863 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id);
862 864
863 return IsPinned(GetShelfIDForAppID(shelf_app_id)); 865 return IsPinned(GetShelfIDForAppID(shelf_app_id));
864 } 866 }
865 867
866 void ChromeLauncherControllerImpl::UnpinAppWithID(const std::string& app_id) { 868 void ChromeLauncherControllerImpl::UnpinAppWithID(const std::string& app_id) {
867 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859 869 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859
868 const std::string shelf_app_id = 870 const std::string shelf_app_id =
869 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id); 871 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(app_id);
870 872
871 // Requests to unpin should only be be made for apps with editable pin states. 873 // Requests to unpin should only be be made for apps with editable pin states.
872 DCHECK_EQ(GetPinnableForAppID(shelf_app_id, profile()), 874 DCHECK_EQ(GetPinnableForAppID(shelf_app_id, profile()),
873 AppListControllerDelegate::PIN_EDITABLE); 875 AppListControllerDelegate::PIN_EDITABLE);
874 876
875 // If the app is already not pinned, do nothing and return. 877 // If the app is pinned, unpin the shelf item (and remove it if not running).
876 if (!IsAppPinned(shelf_app_id)) 878 if (IsAppPinned(shelf_app_id))
877 return; 879 UnpinShelfItemInternal(GetShelfIDForAppID(shelf_app_id));
878
879 // TODO(msw): Trigger pref updates in ShelfModelObserver overrides.
880 ash::launcher::RemovePinPosition(profile(), ash::AppLaunchId(shelf_app_id));
881
882 // Unpin the shelf item.
883 UnpinShelfItemInternal(GetShelfIDForAppID(shelf_app_id));
884 } 880 }
885 881
886 /////////////////////////////////////////////////////////////////////////////// 882 ///////////////////////////////////////////////////////////////////////////////
887 // LauncherAppUpdater::Delegate: 883 // LauncherAppUpdater::Delegate:
888 884
889 void ChromeLauncherControllerImpl::OnAppInstalled( 885 void ChromeLauncherControllerImpl::OnAppInstalled(
890 content::BrowserContext* browser_context, 886 content::BrowserContext* browser_context,
891 const std::string& app_id) { 887 const std::string& app_id) {
892 if (IsAppPinned(app_id)) { 888 if (IsAppPinned(app_id)) {
893 // Clear and re-fetch to ensure icon is up-to-date. 889 // Clear and re-fetch to ensure icon is up-to-date.
(...skipping 135 matching lines...) Expand 10 before | Expand all | Expand 10 after
1029 } 1025 }
1030 1026
1031 void ChromeLauncherControllerImpl::UnpinRunningAppInternal(int index) { 1027 void ChromeLauncherControllerImpl::UnpinRunningAppInternal(int index) {
1032 DCHECK(index >= 0 && index < model_->item_count()); 1028 DCHECK(index >= 0 && index < model_->item_count());
1033 ash::ShelfItem item = model_->items()[index]; 1029 ash::ShelfItem item = model_->items()[index];
1034 DCHECK_EQ(item.type, ash::TYPE_PINNED_APP); 1030 DCHECK_EQ(item.type, ash::TYPE_PINNED_APP);
1035 SetItemType(item.id, ash::TYPE_APP); 1031 SetItemType(item.id, ash::TYPE_APP);
1036 } 1032 }
1037 1033
1038 void ChromeLauncherControllerImpl::SyncPinPosition(ash::ShelfID shelf_id) { 1034 void ChromeLauncherControllerImpl::SyncPinPosition(ash::ShelfID shelf_id) {
1035 DCHECK(should_sync_pin_changes());
1039 DCHECK(shelf_id); 1036 DCHECK(shelf_id);
1040 if (ignore_persist_pinned_state_change_)
1041 return;
1042 1037
1043 const int max_index = model_->item_count(); 1038 const int max_index = model_->item_count();
1044 const int index = model_->ItemIndexByID(shelf_id); 1039 const int index = model_->ItemIndexByID(shelf_id);
1045 DCHECK_GT(index, 0); 1040 DCHECK_GT(index, 0);
1046 1041
1047 const std::string& app_id = GetAppIDForShelfID(shelf_id); 1042 const std::string& app_id = GetAppIDForShelfID(shelf_id);
1048 DCHECK(!app_id.empty()); 1043 DCHECK(!app_id.empty());
1049 const std::string& launch_id = GetLaunchIDForShelfID(shelf_id); 1044 const std::string& launch_id = GetLaunchIDForShelfID(shelf_id);
1050 1045
1051 std::string app_id_before; 1046 std::string app_id_before;
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
1090 } 1085 }
1091 1086
1092 void ChromeLauncherControllerImpl::ScheduleUpdateAppLaunchersFromPref() { 1087 void ChromeLauncherControllerImpl::ScheduleUpdateAppLaunchersFromPref() {
1093 base::ThreadTaskRunnerHandle::Get()->PostTask( 1088 base::ThreadTaskRunnerHandle::Get()->PostTask(
1094 FROM_HERE, 1089 FROM_HERE,
1095 base::Bind(&ChromeLauncherControllerImpl::UpdateAppLaunchersFromPref, 1090 base::Bind(&ChromeLauncherControllerImpl::UpdateAppLaunchersFromPref,
1096 weak_ptr_factory_.GetWeakPtr())); 1091 weak_ptr_factory_.GetWeakPtr()));
1097 } 1092 }
1098 1093
1099 void ChromeLauncherControllerImpl::UpdateAppLaunchersFromPref() { 1094 void ChromeLauncherControllerImpl::UpdateAppLaunchersFromPref() {
1100 // There are various functions which will trigger a |SyncPinPosition| call 1095 // Do not sync pin changes during this function to avoid cyclical updates.
1101 // like a direct call to |PinAppWithID|, or an indirect call to the menu 1096 // This function makes the shelf model reflect synced prefs, and should not
1102 // model which will use weights to re-arrange the icons to new positions. 1097 // cyclically trigger sync changes (eg. ShelfItemAdded calls SyncPinPosition).
1103 // Since this function is meant to synchronize the "is state" with the 1098 ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler();
1104 // "sync state", it makes no sense to store any changes by this function back 1099
1105 // into the pref state. Therefore we tell |persistPinnedState| to ignore any
1106 // invocations while we are running.
1107 base::AutoReset<bool> auto_reset(&ignore_persist_pinned_state_change_, true);
1108 const std::vector<ash::AppLaunchId> pinned_apps = 1100 const std::vector<ash::AppLaunchId> pinned_apps =
1109 ash::launcher::GetPinnedAppsFromPrefs(profile()->GetPrefs(), 1101 ash::launcher::GetPinnedAppsFromPrefs(profile()->GetPrefs(),
1110 launcher_controller_helper()); 1102 launcher_controller_helper());
1111 1103
1112 int index = 0; 1104 int index = 0;
1113 // Skip app list items if it exists. 1105 // Skip app list items if it exists.
1114 if (model_->items()[0].type == ash::TYPE_APP_LIST) 1106 if (model_->items()[0].type == ash::TYPE_APP_LIST)
1115 ++index; 1107 ++index;
1116 1108
1117 // Apply pins in two steps. At the first step, go through the list of apps to 1109 // Apply pins in two steps. At the first step, go through the list of apps to
(...skipping 142 matching lines...) Expand 10 before | Expand all | Expand 10 after
1260 if (app_icon_loader) { 1252 if (app_icon_loader) {
1261 app_icon_loader->FetchImage(app_id); 1253 app_icon_loader->FetchImage(app_id);
1262 app_icon_loader->UpdateImage(app_id); 1254 app_icon_loader->UpdateImage(app_id);
1263 } 1255 }
1264 1256
1265 SetShelfItemDelegate(id, controller); 1257 SetShelfItemDelegate(id, controller);
1266 return id; 1258 return id;
1267 } 1259 }
1268 1260
1269 void ChromeLauncherControllerImpl::CreateBrowserShortcutLauncherItem() { 1261 void ChromeLauncherControllerImpl::CreateBrowserShortcutLauncherItem() {
1262 // Do not sync the pin position of the browser shortcut item when it is added;
1263 // its initial position before prefs have loaded is unimportant and the sync
1264 // service may not yet be initialized.
1265 ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler();
1266
1270 ash::ShelfItem browser_shortcut; 1267 ash::ShelfItem browser_shortcut;
1271 browser_shortcut.type = ash::TYPE_BROWSER_SHORTCUT; 1268 browser_shortcut.type = ash::TYPE_BROWSER_SHORTCUT;
1272 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 1269 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
1273 browser_shortcut.image = *rb.GetImageSkiaNamed(IDR_PRODUCT_LOGO_32); 1270 browser_shortcut.image = *rb.GetImageSkiaNamed(IDR_PRODUCT_LOGO_32);
1274 browser_shortcut.title = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); 1271 browser_shortcut.title = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME);
1275 browser_shortcut.app_launch_id = 1272 browser_shortcut.app_launch_id =
1276 ash::AppLaunchId(extension_misc::kChromeAppId); 1273 ash::AppLaunchId(extension_misc::kChromeAppId);
1277 ash::ShelfID id = model_->next_id(); 1274 ash::ShelfID id = model_->next_id();
1278 model_->AddAt(0, browser_shortcut); 1275 model_->AddAt(0, browser_shortcut);
1279 BrowserShortcutLauncherItemController* controller = 1276 BrowserShortcutLauncherItemController* controller =
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
1354 app_list::AppListSyncableServiceFactory::GetForProfile(profile()); 1351 app_list::AppListSyncableServiceFactory::GetForProfile(profile());
1355 if (app_service) 1352 if (app_service)
1356 app_service->RemoveObserver(this); 1353 app_service->RemoveObserver(this);
1357 1354
1358 PrefServiceSyncableFromProfile(profile())->RemoveObserver(this); 1355 PrefServiceSyncableFromProfile(profile())->RemoveObserver(this);
1359 } 1356 }
1360 1357
1361 /////////////////////////////////////////////////////////////////////////////// 1358 ///////////////////////////////////////////////////////////////////////////////
1362 // ash::ShelfModelObserver: 1359 // ash::ShelfModelObserver:
1363 1360
1364 void ChromeLauncherControllerImpl::ShelfItemAdded(int index) {} 1361 void ChromeLauncherControllerImpl::ShelfItemAdded(int index) {
1362 // Update the pin position preference as needed.
1363 const ash::ShelfItem& item = model_->items()[index];
1364 if (ItemTypeIsPinned(item) && should_sync_pin_changes())
1365 SyncPinPosition(item.id);
1366 }
1365 1367
1366 void ChromeLauncherControllerImpl::ShelfItemRemoved( 1368 void ChromeLauncherControllerImpl::ShelfItemRemoved(
1367 int index, 1369 int index,
1368 const ash::ShelfItem& old_item) { 1370 const ash::ShelfItem& old_item) {
1371 // Remove the pin position from preferences as needed.
1372 if (ItemTypeIsPinned(old_item) && should_sync_pin_changes()) {
1373 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859
1374 const std::string shelf_app_id =
1375 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(
1376 old_item.app_launch_id.app_id());
1377 ash::AppLaunchId app_launch_id(shelf_app_id,
1378 old_item.app_launch_id.launch_id());
1379 ash::launcher::RemovePinPosition(profile(), app_launch_id);
1380 }
1381
1369 // TODO(skuhne): This fixes crbug.com/429870, but it does not answer why we 1382 // TODO(skuhne): This fixes crbug.com/429870, but it does not answer why we
1370 // get into this state in the first place. 1383 // get into this state in the first place.
1371 IDToItemControllerMap::iterator iter = 1384 if (id_to_item_controller_map_.count(old_item.id) > 0)
1372 id_to_item_controller_map_.find(old_item.id); 1385 id_to_item_controller_map_.erase(old_item.id);
1373 if (iter == id_to_item_controller_map_.end())
1374 return;
1375
1376 LOG(ERROR) << "Unexpected removal of shelf item, id: " << old_item.id;
1377 id_to_item_controller_map_.erase(iter);
1378 } 1386 }
1379 1387
1380 void ChromeLauncherControllerImpl::ShelfItemMoved(int start_index, 1388 void ChromeLauncherControllerImpl::ShelfItemMoved(int start_index,
1381 int target_index) { 1389 int target_index) {
1390 // Update the pin position preference as needed.
1382 const ash::ShelfItem& item = model_->items()[target_index]; 1391 const ash::ShelfItem& item = model_->items()[target_index];
1383 // We remember the moved item position if it is either pinnable or
1384 // it is the app list with the alternate shelf layout.
1385 DCHECK_NE(ash::TYPE_APP_LIST, item.type); 1392 DCHECK_NE(ash::TYPE_APP_LIST, item.type);
1386 if (IsPinned(item.id)) 1393 if (ItemTypeIsPinned(item) && should_sync_pin_changes())
1387 SyncPinPosition(item.id); 1394 SyncPinPosition(item.id);
1388 } 1395 }
1389 1396
1390 void ChromeLauncherControllerImpl::ShelfItemChanged( 1397 void ChromeLauncherControllerImpl::ShelfItemChanged(
1391 int index, 1398 int index,
1392 const ash::ShelfItem& old_item) {} 1399 const ash::ShelfItem& old_item) {
1400 if (!should_sync_pin_changes())
1401 return;
1402
1403 const ash::ShelfItem& item = model_->items()[index];
1404 // Add or remove the pin position from preferences as needed.
1405 if (!ItemTypeIsPinned(old_item) && ItemTypeIsPinned(item)) {
1406 SyncPinPosition(item.id);
1407 } else if (ItemTypeIsPinned(old_item) && !ItemTypeIsPinned(item)) {
1408 // TODO(khmel): Fix this Arc application id mapping. See http://b/31703859
1409 const std::string shelf_app_id =
1410 ArcAppWindowLauncherController::GetShelfAppIdFromArcAppId(
1411 old_item.app_launch_id.app_id());
1412
1413 ash::AppLaunchId app_launch_id(shelf_app_id,
1414 old_item.app_launch_id.launch_id());
1415 ash::launcher::RemovePinPosition(profile(), app_launch_id);
1416 }
1417 }
1393 1418
1394 void ChromeLauncherControllerImpl::OnSetShelfItemDelegate( 1419 void ChromeLauncherControllerImpl::OnSetShelfItemDelegate(
1395 ash::ShelfID id, 1420 ash::ShelfID id,
1396 ash::mojom::ShelfItemDelegate* item_delegate) { 1421 ash::mojom::ShelfItemDelegate* item_delegate) {
1397 // TODO(skuhne): This fixes crbug.com/429870, but it does not answer why we 1422 // TODO(skuhne): This fixes crbug.com/429870, but it does not answer why we
1398 // get into this state in the first place. 1423 // get into this state in the first place.
1399 IDToItemControllerMap::iterator iter = id_to_item_controller_map_.find(id); 1424 IDToItemControllerMap::iterator iter = id_to_item_controller_map_.find(id);
1400 if (iter == id_to_item_controller_map_.end() || item_delegate == iter->second) 1425 if (iter == id_to_item_controller_map_.end() || item_delegate == iter->second)
1401 return; 1426 return;
1402 LOG(ERROR) << "Unexpected change of shelf item delegate, id: " << id; 1427 LOG(ERROR) << "Unexpected change of shelf item delegate, id: " << id;
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
1449 LauncherItemController* controller = GetLauncherItemController(item.id); 1474 LauncherItemController* controller = GetLauncherItemController(item.id);
1450 if (!controller || controller->image_set_by_controller()) 1475 if (!controller || controller->image_set_by_controller())
1451 continue; 1476 continue;
1452 item.image = image; 1477 item.image = image;
1453 if (arc_deferred_launcher_) 1478 if (arc_deferred_launcher_)
1454 arc_deferred_launcher_->MaybeApplySpinningEffect(id, &item.image); 1479 arc_deferred_launcher_->MaybeApplySpinningEffect(id, &item.image);
1455 model_->Set(index, item); 1480 model_->Set(index, item);
1456 // It's possible we're waiting on more than one item, so don't break. 1481 // It's possible we're waiting on more than one item, so don't break.
1457 } 1482 }
1458 } 1483 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698