Add warn before quitting experiment on Windows and Linux

This CL adds a warn-before-quitting flag.  When enabled, pressing Ctrl+Shift+Q
will show a bubble asking to continue holding the shortcut to quit.  Pressing
the shortcut a second time while the bubble is showing will also confirm the
quit.

R=sky
BUG=243164

Change-Id: I97c4dc37cb5107fde975a4162f349e1ea5337b5d
Reviewed-on: https://chromium-review.googlesource.com/1031097
Reviewed-by: Scott Violet <sky@chromium.org>
Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
Cr-Commit-Position: refs/heads/master@{#555909}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index f583e30..537eff0d 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -2447,6 +2447,11 @@
      flag_descriptions::kEnableInputImeApiDescription, kOsWin | kOsLinux,
      ENABLE_DISABLE_VALUE_TYPE(switches::kEnableInputImeAPI,
                                switches::kDisableInputImeAPI)},
+#if !defined(OS_CHROMEOS)
+    {"warn-before-quitting", flag_descriptions::kWarnBeforeQuittingFlagName,
+     flag_descriptions::kWarnBeforeQuittingFlagDescription, kOsWin | kOsLinux,
+     FEATURE_VALUE_TYPE(features::kWarnBeforeQuitting)},
+#endif  // OS_CHROMEOS
 #endif  // OS_WIN || OS_LINUX
     {"enable-origin-trials", flag_descriptions::kOriginTrialsName,
      flag_descriptions::kOriginTrialsDescription, kOsAll,
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 11b40d03..4922451 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -3026,6 +3026,14 @@
 const char kEnableInputImeApiDescription[] =
     "Enable the use of chrome.input.ime API.";
 
+#if !defined(OS_CHROMEOS)
+
+const char kWarnBeforeQuittingFlagName[] = "Warn Before Quitting";
+const char kWarnBeforeQuittingFlagDescription[] =
+    "Confirm to quit by either holding the quit shortcut or pressing it twice.";
+
+#endif  // !defined(OS_CHROMEOS)
+
 #endif  // defined(OS_WIN) || defined(OS_LINUX)
 
 #if defined(OS_WIN) || defined(OS_MACOSX)
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 9949be4..d69510e 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1851,6 +1851,13 @@
 extern const char kEnableInputImeApiName[];
 extern const char kEnableInputImeApiDescription[];
 
+#if !defined(OS_CHROMEOS)
+
+extern const char kWarnBeforeQuittingFlagName[];
+extern const char kWarnBeforeQuittingFlagDescription[];
+
+#endif  // !defined(OS_CHROMEOS)
+
 #endif  // defined(OS_WIN) || defined(OS_LINUX)
 
 #if defined(OS_WIN) || defined(OS_MACOSX)
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 391a9a1..e0ec670 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -2281,6 +2281,11 @@
       sources += [
         "input_method/input_method_engine.cc",
         "input_method/input_method_engine.h",
+        "views/confirm_quit_bubble.cc",
+        "views/confirm_quit_bubble.h",
+        "views/confirm_quit_bubble.h",
+        "views/confirm_quit_bubble_controller.cc",
+        "views/confirm_quit_bubble_controller.h",
       ]
     }
   }
diff --git a/chrome/browser/ui/views/confirm_quit_bubble.cc b/chrome/browser/ui/views/confirm_quit_bubble.cc
new file mode 100644
index 0000000..610a92a72
--- /dev/null
+++ b/chrome/browser/ui/views/confirm_quit_bubble.cc
@@ -0,0 +1,76 @@
+// Copyright 2018 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/ui/views/confirm_quit_bubble.h"
+
+#include <utility>
+
+#include "base/message_loop/message_loop.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/ui/views/subtle_notification_view.h"
+#include "ui/gfx/animation/animation.h"
+#include "ui/gfx/animation/slide_animation.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/size.h"
+#include "ui/strings/grit/ui_strings.h"
+#include "ui/views/border.h"
+#include "ui/views/view.h"
+#include "ui/views/widget/widget.h"
+
+namespace {
+
+constexpr base::TimeDelta kSlideDuration =
+    base::TimeDelta::FromMilliseconds(200);
+
+}  // namespace
+
+ConfirmQuitBubble::ConfirmQuitBubble()
+    : animation_(std::make_unique<gfx::SlideAnimation>(this)) {
+  animation_->SetSlideDuration(kSlideDuration.InMilliseconds());
+}
+
+ConfirmQuitBubble::~ConfirmQuitBubble() {}
+
+void ConfirmQuitBubble::Show() {
+  animation_->Show();
+}
+
+void ConfirmQuitBubble::Hide() {
+  animation_->Hide();
+}
+
+void ConfirmQuitBubble::AnimationProgressed(const gfx::Animation* animation) {
+  float opacity = static_cast<float>(animation->CurrentValueBetween(0.0, 1.0));
+  if (opacity == 0) {
+    popup_.reset();
+  } else {
+    if (!popup_) {
+      SubtleNotificationView* view = new SubtleNotificationView();
+
+      popup_ = std::make_unique<views::Widget>();
+      views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
+      params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
+      params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
+      params.accept_events = false;
+      params.keep_on_top = true;
+      popup_->Init(params);
+      popup_->SetContentsView(view);
+
+      // TODO(thomasanderson): Localize this string.
+      view->UpdateContent(
+          base::WideToUTF16(L"Hold |Ctrl|+|Shift|+|Q| to quit"));
+
+      gfx::Size size = view->GetPreferredSize();
+      view->SetSize(size);
+      popup_->CenterWindow(size);
+
+      popup_->ShowInactive();
+    }
+    popup_->SetOpacity(opacity);
+  }
+}
+
+void ConfirmQuitBubble::AnimationEnded(const gfx::Animation* animation) {
+  AnimationProgressed(animation);
+}
diff --git a/chrome/browser/ui/views/confirm_quit_bubble.h b/chrome/browser/ui/views/confirm_quit_bubble.h
new file mode 100644
index 0000000..b4de3d0
--- /dev/null
+++ b/chrome/browser/ui/views/confirm_quit_bubble.h
@@ -0,0 +1,47 @@
+// Copyright 2018 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.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_H_
+#define CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_H_
+
+#include <memory>
+
+#include "base/macros.h"
+#include "chrome/browser/ui/views/confirm_quit_bubble_base.h"
+#include "ui/gfx/animation/animation_delegate.h"
+
+namespace gfx {
+class Animation;
+class SlideAnimation;
+}  // namespace gfx
+
+namespace views {
+class Widget;
+}  // namespace views
+
+// Manages showing and hiding a notification bubble that gives instructions to
+// continue holding the quit accelerator to quit.
+class ConfirmQuitBubble : public ConfirmQuitBubbleBase,
+                          public gfx::AnimationDelegate {
+ public:
+  ConfirmQuitBubble();
+  ~ConfirmQuitBubble() override;
+
+  void Show() override;
+  void Hide() override;
+
+ private:
+  // gfx::AnimationDelegate:
+  void AnimationProgressed(const gfx::Animation* animation) override;
+  void AnimationEnded(const gfx::Animation* animation) override;
+
+  // Animation controlling showing/hiding of the bubble.
+  std::unique_ptr<gfx::SlideAnimation> const animation_;
+
+  std::unique_ptr<views::Widget> popup_;
+
+  DISALLOW_COPY_AND_ASSIGN(ConfirmQuitBubble);
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_H_
diff --git a/chrome/browser/ui/views/confirm_quit_bubble_base.h b/chrome/browser/ui/views/confirm_quit_bubble_base.h
new file mode 100644
index 0000000..8bd73c2
--- /dev/null
+++ b/chrome/browser/ui/views/confirm_quit_bubble_base.h
@@ -0,0 +1,19 @@
+// Copyright 2018 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.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_BASE_H_
+#define CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_BASE_H_
+
+// Base class of ConfirmQuitBubble necessary for unit testing
+// ConfirmQuitBubbleController.
+class ConfirmQuitBubbleBase {
+ public:
+  ConfirmQuitBubbleBase() {}
+  virtual ~ConfirmQuitBubbleBase() {}
+
+  virtual void Show() = 0;
+  virtual void Hide() = 0;
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_BASE_H_
diff --git a/chrome/browser/ui/views/confirm_quit_bubble_controller.cc b/chrome/browser/ui/views/confirm_quit_bubble_controller.cc
new file mode 100644
index 0000000..ab391c1
--- /dev/null
+++ b/chrome/browser/ui/views/confirm_quit_bubble_controller.cc
@@ -0,0 +1,94 @@
+// Copyright 2018 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/ui/views/confirm_quit_bubble_controller.h"
+
+#include <utility>
+
+#include "base/feature_list.h"
+#include "base/memory/singleton.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/views/confirm_quit_bubble.h"
+#include "ui/base/accelerators/accelerator.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+
+namespace {
+
+constexpr ui::KeyboardCode kAcceleratorKeyCode = ui::VKEY_Q;
+constexpr int kAcceleratorModifiers = ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN;
+
+constexpr base::TimeDelta kShowDuration =
+    base::TimeDelta::FromMilliseconds(1500);
+
+}  // namespace
+
+// static
+ConfirmQuitBubbleController* ConfirmQuitBubbleController::GetInstance() {
+  return base::Singleton<ConfirmQuitBubbleController>::get();
+}
+
+ConfirmQuitBubbleController::ConfirmQuitBubbleController()
+    : view_(std::make_unique<ConfirmQuitBubble>()),
+      hide_timer_(std::make_unique<base::OneShotTimer>()) {}
+
+ConfirmQuitBubbleController::ConfirmQuitBubbleController(
+    std::unique_ptr<ConfirmQuitBubbleBase> bubble,
+    std::unique_ptr<base::Timer> hide_timer)
+    : view_(std::move(bubble)), hide_timer_(std::move(hide_timer)) {}
+
+ConfirmQuitBubbleController::~ConfirmQuitBubbleController() {}
+
+bool ConfirmQuitBubbleController::HandleKeyboardEvent(
+    const ui::Accelerator& accelerator) {
+  if (accelerator.key_code() == kAcceleratorKeyCode &&
+      accelerator.modifiers() == kAcceleratorModifiers &&
+      accelerator.key_state() == ui::Accelerator::KeyState::PRESSED &&
+      !accelerator.IsRepeat()) {
+    if (!hide_timer_->IsRunning()) {
+      view_->Show();
+      released_key_ = false;
+      hide_timer_->Start(FROM_HERE, kShowDuration, this,
+                         &ConfirmQuitBubbleController::OnTimerElapsed);
+    } else {
+      // The accelerator was pressed while the bubble was showing.  Consider
+      // this a confirmation to quit.
+      view_->Hide();
+      hide_timer_->Stop();
+      Quit();
+    }
+    return true;
+  }
+  if (accelerator.key_code() == kAcceleratorKeyCode &&
+      accelerator.key_state() == ui::Accelerator::KeyState::RELEASED) {
+    released_key_ = true;
+    return true;
+  }
+  return false;
+}
+
+void ConfirmQuitBubbleController::OnTimerElapsed() {
+  view_->Hide();
+
+  if (!released_key_) {
+    // The accelerator was held down the entire time the bubble was showing.
+    Quit();
+  }
+}
+
+void ConfirmQuitBubbleController::Quit() {
+  if (quit_action_) {
+    std::move(quit_action_).Run();
+  } else {
+    // Delay quitting because doing so destroys objects that may be used when
+    // unwinding the stack.
+    base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
+                                                  base::BindOnce(chrome::Exit));
+  }
+}
+
+void ConfirmQuitBubbleController::SetQuitActionForTest(
+    base::OnceClosure quit_action) {
+  quit_action_ = std::move(quit_action);
+}
diff --git a/chrome/browser/ui/views/confirm_quit_bubble_controller.h b/chrome/browser/ui/views/confirm_quit_bubble_controller.h
new file mode 100644
index 0000000..4357d38
--- /dev/null
+++ b/chrome/browser/ui/views/confirm_quit_bubble_controller.h
@@ -0,0 +1,62 @@
+// Copyright 2018 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.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_CONTROLLER_H_
+#define CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_CONTROLLER_H_
+
+#include <memory>
+
+#include "base/macros.h"
+#include "base/timer/timer.h"
+
+class ConfirmQuitBubbleBase;
+
+namespace base {
+template <typename T>
+struct DefaultSingletonTraits;
+}
+
+namespace ui {
+class Accelerator;
+}
+
+// Manages showing and hiding the confirm-to-quit bubble.  Requests Chrome to be
+// closed if the quit accelerator is held down or pressed twice in succession.
+class ConfirmQuitBubbleController {
+ public:
+  static ConfirmQuitBubbleController* GetInstance();
+
+  ~ConfirmQuitBubbleController();
+
+  // Returns true if the event was handled.
+  bool HandleKeyboardEvent(const ui::Accelerator& accelerator);
+
+ private:
+  friend struct base::DefaultSingletonTraits<ConfirmQuitBubbleController>;
+  friend class ConfirmQuitBubbleControllerTest;
+
+  ConfirmQuitBubbleController(std::unique_ptr<ConfirmQuitBubbleBase> bubble,
+                              std::unique_ptr<base::Timer> hide_timer);
+
+  ConfirmQuitBubbleController();
+
+  void OnTimerElapsed();
+
+  void Quit();
+
+  void SetQuitActionForTest(base::OnceClosure quit_action);
+
+  std::unique_ptr<ConfirmQuitBubbleBase> const view_;
+
+  // Indicates if the accelerator was released while the timer was active.
+  bool released_key_ = false;
+
+  std::unique_ptr<base::Timer> hide_timer_;
+
+  base::OnceClosure quit_action_;
+
+  DISALLOW_COPY_AND_ASSIGN(ConfirmQuitBubbleController);
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_CONFIRM_QUIT_BUBBLE_CONTROLLER_H_
diff --git a/chrome/browser/ui/views/confirm_quit_bubble_controller_unittest.cc b/chrome/browser/ui/views/confirm_quit_bubble_controller_unittest.cc
new file mode 100644
index 0000000..e88cc753
--- /dev/null
+++ b/chrome/browser/ui/views/confirm_quit_bubble_controller_unittest.cc
@@ -0,0 +1,130 @@
+// Copyright 2018 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/ui/views/confirm_quit_bubble_controller.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/macros.h"
+#include "base/timer/mock_timer.h"
+#include "chrome/browser/ui/views/confirm_quit_bubble.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/accelerators/accelerator.h"
+#include "ui/events/keycodes/keyboard_codes.h"
+
+class TestConfirmQuitBubble : public ConfirmQuitBubbleBase {
+ public:
+  TestConfirmQuitBubble() {}
+  ~TestConfirmQuitBubble() override {}
+
+  void Show() override {}
+  void Hide() override {}
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(TestConfirmQuitBubble);
+};
+
+class ConfirmQuitBubbleControllerTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    std::unique_ptr<TestConfirmQuitBubble> bubble =
+        std::make_unique<TestConfirmQuitBubble>();
+    std::unique_ptr<base::MockTimer> timer =
+        std::make_unique<base::MockTimer>(false, false);
+    bubble_ = bubble.get();
+    timer_ = timer.get();
+    controller_.reset(
+        new ConfirmQuitBubbleController(std::move(bubble), std::move(timer)));
+
+    quit_called_ = false;
+    controller_->SetQuitActionForTest(base::BindOnce(
+        &ConfirmQuitBubbleControllerTest::OnQuit, base::Unretained(this)));
+  }
+
+  void TearDown() override { controller_.reset(); }
+
+  void OnQuit() { quit_called_ = true; }
+
+  void SendAccelerator(bool quit, bool press, bool repeat) {
+    ui::KeyboardCode key = quit ? ui::VKEY_Q : ui::VKEY_P;
+    int modifiers = ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN;
+    if (repeat)
+      modifiers |= ui::EF_IS_REPEAT;
+    ui::Accelerator::KeyState state = press
+                                          ? ui::Accelerator::KeyState::PRESSED
+                                          : ui::Accelerator::KeyState::RELEASED;
+    controller_->HandleKeyboardEvent(ui::Accelerator(key, modifiers, state));
+  }
+
+  void PressQuitAccelerator() { SendAccelerator(true, true, false); }
+
+  void ReleaseQuitAccelerator() { SendAccelerator(true, false, false); }
+
+  void RepeatQuitAccelerator() { SendAccelerator(true, true, true); }
+
+  void PressOtherAccelerator() { SendAccelerator(false, true, false); }
+
+  void ReleaseOtherAccelerator() { SendAccelerator(false, false, false); }
+
+  std::unique_ptr<ConfirmQuitBubbleController> controller_;
+
+  // Owned by |controller_|.
+  TestConfirmQuitBubble* bubble_;
+
+  // Owned by |controller_|.
+  base::MockTimer* timer_;
+
+  bool quit_called_ = false;
+};
+
+// Pressing and holding the shortcut should quit.
+TEST_F(ConfirmQuitBubbleControllerTest, PressAndHold) {
+  PressQuitAccelerator();
+  EXPECT_TRUE(timer_->IsRunning());
+  timer_->Fire();
+  EXPECT_TRUE(quit_called_);
+}
+
+// Pressing the shortcut twice should quit.
+TEST_F(ConfirmQuitBubbleControllerTest, DoublePress) {
+  PressQuitAccelerator();
+  ReleaseQuitAccelerator();
+  EXPECT_TRUE(timer_->IsRunning());
+  PressQuitAccelerator();
+  EXPECT_FALSE(timer_->IsRunning());
+  EXPECT_TRUE(quit_called_);
+}
+
+// Pressing the shortcut once should not quit.
+TEST_F(ConfirmQuitBubbleControllerTest, SinglePress) {
+  PressQuitAccelerator();
+  ReleaseQuitAccelerator();
+  EXPECT_TRUE(timer_->IsRunning());
+  timer_->Fire();
+  EXPECT_FALSE(quit_called_);
+}
+
+// Repeated presses should not be counted.
+TEST_F(ConfirmQuitBubbleControllerTest, RepeatedPresses) {
+  PressQuitAccelerator();
+  RepeatQuitAccelerator();
+  ReleaseQuitAccelerator();
+  EXPECT_TRUE(timer_->IsRunning());
+  timer_->Fire();
+  EXPECT_FALSE(quit_called_);
+}
+
+// Other keys shouldn't matter.
+TEST_F(ConfirmQuitBubbleControllerTest, OtherKeyPress) {
+  PressQuitAccelerator();
+  ReleaseQuitAccelerator();
+  PressOtherAccelerator();
+  ReleaseOtherAccelerator();
+  EXPECT_TRUE(timer_->IsRunning());
+  PressQuitAccelerator();
+  EXPECT_FALSE(timer_->IsRunning());
+  EXPECT_TRUE(quit_called_);
+}
diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
index d2394950..d5563e1 100644
--- a/chrome/browser/ui/views/frame/browser_view.cc
+++ b/chrome/browser/ui/views/frame/browser_view.cc
@@ -64,6 +64,7 @@
 #include "chrome/browser/ui/views/autofill/save_card_icon_view.h"
 #include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
 #include "chrome/browser/ui/views/bookmarks/bookmark_bubble_view.h"
+#include "chrome/browser/ui/views/confirm_quit_bubble_controller.h"
 #include "chrome/browser/ui/views/download/download_in_progress_dialog_view.h"
 #include "chrome/browser/ui/views/download/download_shelf_view.h"
 #include "chrome/browser/ui/views/exclusive_access_bubble_views.h"
@@ -95,6 +96,7 @@
 #include "chrome/browser/ui/views/update_recommended_message_box.h"
 #include "chrome/browser/ui/window_sizer/window_sizer.h"
 #include "chrome/common/channel_info.h"
+#include "chrome/common/chrome_features.h"
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/extensions/command.h"
 #include "chrome/common/pref_names.h"
@@ -1323,6 +1325,14 @@
     return content::KeyboardEventProcessingResult::NOT_HANDLED;
   }
 
+#if defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
+  if (base::FeatureList::IsEnabled(features::kWarnBeforeQuitting) &&
+      ConfirmQuitBubbleController::GetInstance()->HandleKeyboardEvent(
+          accelerator)) {
+    return content::KeyboardEventProcessingResult::HANDLED_WANTS_KEY_UP;
+  }
+#endif  // defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
+
 #if defined(OS_CHROMEOS)
   if (ash_util::IsAcceleratorDeprecated(accelerator)) {
     return (event.GetType() == blink::WebInputEvent::kRawKeyDown)
@@ -2089,6 +2099,14 @@
 // BrowserView, ui::AcceleratorTarget overrides:
 
 bool BrowserView::AcceleratorPressed(const ui::Accelerator& accelerator) {
+#if defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
+  if (base::FeatureList::IsEnabled(features::kWarnBeforeQuitting) &&
+      ConfirmQuitBubbleController::GetInstance()->HandleKeyboardEvent(
+          accelerator)) {
+    return true;
+  }
+#endif  // defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
+
   int command_id;
   // Though AcceleratorManager should not send unknown |accelerator| to us, it's
   // still possible the command cannot be executed now.
diff --git a/chrome/common/chrome_features.cc b/chrome/common/chrome_features.cc
index 8eb2170..9569d92 100644
--- a/chrome/common/chrome_features.cc
+++ b/chrome/common/chrome_features.cc
@@ -363,6 +363,11 @@
     "AcknowledgeNtpOverrideOnDeactivate", base::FEATURE_DISABLED_BY_DEFAULT};
 #endif
 
+#if defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
+const base::Feature kWarnBeforeQuitting{"WarnBeforeQuitting",
+                                        base::FEATURE_DISABLED_BY_DEFAULT};
+#endif
+
 // The material redesign of the Incognito NTP.
 const base::Feature kMaterialDesignIncognitoNTP{
   "MaterialDesignIncognitoNTP",
diff --git a/chrome/common/chrome_features.h b/chrome/common/chrome_features.h
index 20b7000..42958c3 100644
--- a/chrome/common/chrome_features.h
+++ b/chrome/common/chrome_features.h
@@ -202,6 +202,10 @@
 extern const base::Feature kAcknowledgeNtpOverrideOnDeactivate;
 #endif
 
+#if defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
+extern const base::Feature kWarnBeforeQuitting;
+#endif
+
 extern const base::Feature kMaterialDesignIncognitoNTP;
 
 #if !defined(OS_ANDROID)
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 4545de1..bbe48c9 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -3189,6 +3189,11 @@
     }
   }
 
+  if (is_win || (is_linux && !is_chromeos)) {
+    sources +=
+        [ "../browser/ui/views/confirm_quit_bubble_controller_unittest.cc" ]
+  }
+
   if (enable_native_notifications) {
     if (is_desktop_linux) {
       sources += [ "../browser/notifications/notification_platform_bridge_linux_unittest.cc" ]
diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc
index 190a4c0b..d08b0982 100644
--- a/content/browser/renderer_host/render_widget_host_impl.cc
+++ b/content/browser/renderer_host/render_widget_host_impl.cc
@@ -1435,6 +1435,9 @@
     switch (delegate_->PreHandleKeyboardEvent(key_event)) {
       case KeyboardEventProcessingResult::HANDLED:
         return;
+      case KeyboardEventProcessingResult::HANDLED_WANTS_KEY_UP:
+        suppress_events_until_keydown_ = false;
+        return;
 #if defined(USE_AURA)
       case KeyboardEventProcessingResult::HANDLED_DONT_UPDATE_EVENT:
         if (update_event)
diff --git a/content/public/browser/keyboard_event_processing_result.h b/content/public/browser/keyboard_event_processing_result.h
index 4030a52..ba43542c 100644
--- a/content/public/browser/keyboard_event_processing_result.h
+++ b/content/public/browser/keyboard_event_processing_result.h
@@ -11,6 +11,9 @@
   // The event was handled.
   HANDLED,
 
+  // The event was handled, and we want to be notified of the keyup event too.
+  HANDLED_WANTS_KEY_UP,
+
 #if defined(USE_AURA)
   // The event was handled, but don't update the underlying event. A value
   // HANDLED results in calling ui::Event::SetHandled(), where as this does not.
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 4127b44..d7a3bdc 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -27421,6 +27421,7 @@
   <int value="423855924" label="enable-tab-switcher-theme-colors"/>
   <int value="431691805" label="MediaDocumentDownloadButton:enabled"/>
   <int value="434033638" label="PwaPersistentNotification:disabled"/>
+  <int value="438048339" label="WarnBeforeQuitting:disabled"/>
   <int value="446316019" label="enable-threaded-compositing"/>
   <int value="451196246" label="disable-impl-side-painting"/>
   <int value="452139294" label="VrShellExperimentalRendering:enabled"/>
@@ -27460,6 +27461,7 @@
   <int value="513356954" label="InstantTethering:disabled"/>
   <int value="513372959" label="ViewsProfileChooser:enabled"/>
   <int value="517568645" label="AnimatedAppMenuIcon:disabled"/>
+  <int value="530158943" label="WarnBeforeQuitting:enabled"/>
   <int value="535131384" label="OmniboxTailSuggestions:enabled"/>
   <int value="535976218" label="enable-plugin-power-saver"/>
   <int value="538468149" label="OfflinePagesCT:enabled"/>