Select/deselect and isIndexSelected for Comboboxes and Listboxes.

Part of feature work to make Pdfium more usable by mobile platforms
that wish to render comboboxes and listboxes using custom displays
when a widget is selected instead of Pdfium's support for drawing
the dropdown/selected items.

R=thestig@chromium.org

Bug: b/124253371
Change-Id: Idd1759bd7a7ab2ae518583e9325dfd094d91c8ad
Reviewed-on: https://pdfium-review.googlesource.com/c/50650
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/fpdfsdk/cpdfsdk_annothandlermgr.cpp b/fpdfsdk/cpdfsdk_annothandlermgr.cpp
index 619d66d..0061eb5 100644
--- a/fpdfsdk/cpdfsdk_annothandlermgr.cpp
+++ b/fpdfsdk/cpdfsdk_annothandlermgr.cpp
@@ -255,6 +255,20 @@
   return GetAnnotHandler(pAnnot->Get())->OnKillFocus(pAnnot, nFlag);
 }
 
+bool CPDFSDK_AnnotHandlerMgr::Annot_SetIndexSelected(
+    CPDFSDK_Annot::ObservedPtr* pAnnot,
+    int index,
+    bool selected) {
+  return GetAnnotHandler(pAnnot->Get())
+      ->SetIndexSelected(pAnnot, index, selected);
+}
+
+bool CPDFSDK_AnnotHandlerMgr::Annot_IsIndexSelected(
+    CPDFSDK_Annot::ObservedPtr* pAnnot,
+    int index) {
+  return GetAnnotHandler(pAnnot->Get())->IsIndexSelected(pAnnot, index);
+}
+
 #ifdef PDF_ENABLE_XFA
 bool CPDFSDK_AnnotHandlerMgr::Annot_OnChangeFocus(
     CPDFSDK_Annot::ObservedPtr* pSetAnnot,
diff --git a/fpdfsdk/cpdfsdk_annothandlermgr.h b/fpdfsdk/cpdfsdk_annothandlermgr.h
index eb8ef48..bb1e8c3 100644
--- a/fpdfsdk/cpdfsdk_annothandlermgr.h
+++ b/fpdfsdk/cpdfsdk_annothandlermgr.h
@@ -94,6 +94,10 @@
   bool Annot_OnKeyDown(CPDFSDK_Annot* pAnnot, int nKeyCode, int nFlag);
   bool Annot_OnSetFocus(CPDFSDK_Annot::ObservedPtr* pAnnot, uint32_t nFlag);
   bool Annot_OnKillFocus(CPDFSDK_Annot::ObservedPtr* pAnnot, uint32_t nFlag);
+  bool Annot_SetIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                              int index,
+                              bool selected);
+  bool Annot_IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot, int index);
 
 #ifdef PDF_ENABLE_XFA
   bool Annot_OnChangeFocus(CPDFSDK_Annot::ObservedPtr* pSetAnnot,
diff --git a/fpdfsdk/cpdfsdk_baannothandler.cpp b/fpdfsdk/cpdfsdk_baannothandler.cpp
index 168c806..b129e15 100644
--- a/fpdfsdk/cpdfsdk_baannothandler.cpp
+++ b/fpdfsdk/cpdfsdk_baannothandler.cpp
@@ -180,6 +180,18 @@
   return false;
 }
 
+bool CPDFSDK_BAAnnotHandler::SetIndexSelected(
+    CPDFSDK_Annot::ObservedPtr* pAnnot,
+    int index,
+    bool selected) {
+  return false;
+}
+
+bool CPDFSDK_BAAnnotHandler::IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                                             int index) {
+  return false;
+}
+
 #ifdef PDF_ENABLE_XFA
 bool CPDFSDK_BAAnnotHandler::OnXFAChangedFocus(
     CPDFSDK_Annot::ObservedPtr* pOldAnnot,
diff --git a/fpdfsdk/cpdfsdk_baannothandler.h b/fpdfsdk/cpdfsdk_baannothandler.h
index 5adabc2..8154a44 100644
--- a/fpdfsdk/cpdfsdk_baannothandler.h
+++ b/fpdfsdk/cpdfsdk_baannothandler.h
@@ -97,6 +97,10 @@
   bool OnKeyUp(CPDFSDK_Annot* pAnnot, int nKeyCode, int nFlag) override;
   bool OnSetFocus(CPDFSDK_Annot::ObservedPtr* pAnnot, uint32_t nFlag) override;
   bool OnKillFocus(CPDFSDK_Annot::ObservedPtr* pAnnot, uint32_t nFlag) override;
+  bool SetIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                        int index,
+                        bool selected) override;
+  bool IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot, int index) override;
 #ifdef PDF_ENABLE_XFA
   bool OnXFAChangedFocus(CPDFSDK_Annot::ObservedPtr* pOldAnnot,
                          CPDFSDK_Annot::ObservedPtr* pNewAnnot) override;
diff --git a/fpdfsdk/cpdfsdk_pageview.cpp b/fpdfsdk/cpdfsdk_pageview.cpp
index b0b5f8a..def4731 100644
--- a/fpdfsdk/cpdfsdk_pageview.cpp
+++ b/fpdfsdk/cpdfsdk_pageview.cpp
@@ -441,6 +441,29 @@
                                               static_cast<int>(deltaY), point);
 }
 
+bool CPDFSDK_PageView::SetIndexSelected(int index, bool selected) {
+  if (CPDFSDK_Annot* pAnnot = GetFocusAnnot()) {
+    CPDFSDK_Annot::ObservedPtr pAnnotObserved(pAnnot);
+    CPDFSDK_AnnotHandlerMgr* pAnnotHandlerMgr =
+        m_pFormFillEnv->GetAnnotHandlerMgr();
+    return pAnnotHandlerMgr->Annot_SetIndexSelected(&pAnnotObserved, index,
+                                                    selected);
+  }
+
+  return false;
+}
+
+bool CPDFSDK_PageView::IsIndexSelected(int index) {
+  if (CPDFSDK_Annot* pAnnot = GetFocusAnnot()) {
+    CPDFSDK_Annot::ObservedPtr pAnnotObserved(pAnnot);
+    CPDFSDK_AnnotHandlerMgr* pAnnotHandlerMgr =
+        m_pFormFillEnv->GetAnnotHandlerMgr();
+    return pAnnotHandlerMgr->Annot_IsIndexSelected(&pAnnotObserved, index);
+  }
+
+  return false;
+}
+
 bool CPDFSDK_PageView::OnChar(int nChar, uint32_t nFlag) {
   if (CPDFSDK_Annot* pAnnot = GetFocusAnnot()) {
     CPDFSDK_AnnotHandlerMgr* pAnnotHandlerMgr =
diff --git a/fpdfsdk/cpdfsdk_pageview.h b/fpdfsdk/cpdfsdk_pageview.h
index d877a70..2ee15c3 100644
--- a/fpdfsdk/cpdfsdk_pageview.h
+++ b/fpdfsdk/cpdfsdk_pageview.h
@@ -82,6 +82,9 @@
                     const CFX_PointF& point,
                     int nFlag);
 
+  bool SetIndexSelected(int index, bool selected);
+  bool IsIndexSelected(int index);
+
   const CFX_Matrix& GetCurrentMatrix() const { return m_curMatrix; }
   void UpdateRects(const std::vector<CFX_FloatRect>& rects);
   void UpdateView(CPDFSDK_Annot* pAnnot);
diff --git a/fpdfsdk/cpdfsdk_widgethandler.cpp b/fpdfsdk/cpdfsdk_widgethandler.cpp
index 80f60cf..47a8934 100644
--- a/fpdfsdk/cpdfsdk_widgethandler.cpp
+++ b/fpdfsdk/cpdfsdk_widgethandler.cpp
@@ -240,6 +240,19 @@
          m_pFormFiller->OnKillFocus(pAnnot, nFlag);
 }
 
+bool CPDFSDK_WidgetHandler::SetIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                                             int index,
+                                             bool selected) {
+  return !(*pAnnot)->IsSignatureWidget() &&
+         m_pFormFiller->SetIndexSelected(pAnnot, index, selected);
+}
+
+bool CPDFSDK_WidgetHandler::IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                                            int index) {
+  return !(*pAnnot)->IsSignatureWidget() &&
+         m_pFormFiller->IsIndexSelected(pAnnot, index);
+}
+
 #ifdef PDF_ENABLE_XFA
 bool CPDFSDK_WidgetHandler::OnXFAChangedFocus(
     CPDFSDK_Annot::ObservedPtr* pOldAnnot,
diff --git a/fpdfsdk/cpdfsdk_widgethandler.h b/fpdfsdk/cpdfsdk_widgethandler.h
index 61b3c54..6997f2b 100644
--- a/fpdfsdk/cpdfsdk_widgethandler.h
+++ b/fpdfsdk/cpdfsdk_widgethandler.h
@@ -98,6 +98,10 @@
   bool OnKeyUp(CPDFSDK_Annot* pAnnot, int nKeyCode, int nFlag) override;
   bool OnSetFocus(CPDFSDK_Annot::ObservedPtr* pAnnot, uint32_t nFlag) override;
   bool OnKillFocus(CPDFSDK_Annot::ObservedPtr* pAnnot, uint32_t nFlag) override;
+  bool SetIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                        int index,
+                        bool selected) override;
+  bool IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot, int index) override;
 #ifdef PDF_ENABLE_XFA
   bool OnXFAChangedFocus(CPDFSDK_Annot::ObservedPtr* pOldAnnot,
                          CPDFSDK_Annot::ObservedPtr* pNewAnnot) override;
diff --git a/fpdfsdk/cpdfsdk_xfawidgethandler.cpp b/fpdfsdk/cpdfsdk_xfawidgethandler.cpp
index fcbfae8..219e1de 100644
--- a/fpdfsdk/cpdfsdk_xfawidgethandler.cpp
+++ b/fpdfsdk/cpdfsdk_xfawidgethandler.cpp
@@ -375,6 +375,19 @@
   return bRet;
 }
 
+bool CPDFSDK_XFAWidgetHandler::SetIndexSelected(
+    CPDFSDK_Annot::ObservedPtr* pAnnot,
+    int index,
+    bool selected) {
+  return false;
+}
+
+bool CPDFSDK_XFAWidgetHandler::IsIndexSelected(
+    CPDFSDK_Annot::ObservedPtr* pAnnot,
+    int index) {
+  return false;
+}
+
 CXFA_FFWidgetHandler* CPDFSDK_XFAWidgetHandler::GetXFAWidgetHandler(
     CPDFSDK_Annot* pAnnot) {
   if (!pAnnot)
diff --git a/fpdfsdk/cpdfsdk_xfawidgethandler.h b/fpdfsdk/cpdfsdk_xfawidgethandler.h
index 5b68556..e00f8a1 100644
--- a/fpdfsdk/cpdfsdk_xfawidgethandler.h
+++ b/fpdfsdk/cpdfsdk_xfawidgethandler.h
@@ -94,6 +94,10 @@
   bool OnKillFocus(CPDFSDK_Annot::ObservedPtr* pAnnot, uint32_t nFlag) override;
   bool OnXFAChangedFocus(CPDFSDK_Annot::ObservedPtr* pOldAnnot,
                          CPDFSDK_Annot::ObservedPtr* pNewAnnot) override;
+  bool SetIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                        int index,
+                        bool selected) override;
+  bool IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot, int index) override;
 
  private:
   CXFA_FFWidgetHandler* GetXFAWidgetHandler(CPDFSDK_Annot* pAnnot);
diff --git a/fpdfsdk/formfiller/cffl_combobox.cpp b/fpdfsdk/formfiller/cffl_combobox.cpp
index ce3cf7d..a27ec51 100644
--- a/fpdfsdk/formfiller/cffl_combobox.cpp
+++ b/fpdfsdk/formfiller/cffl_combobox.cpp
@@ -228,6 +228,43 @@
   }
 }
 
+bool CFFL_ComboBox::SetIndexSelected(int index, bool selected) {
+  if (!IsValid() || !selected)
+    return false;
+
+  if (index < 0 || index >= m_pWidget->CountOptions())
+    return false;
+
+  CPDFSDK_PageView* pPageView = GetCurPageView(true);
+  ASSERT(pPageView);
+
+  CPWL_ComboBox* pWnd =
+      static_cast<CPWL_ComboBox*>(GetPDFWindow(pPageView, false));
+  if (!pWnd)
+    return false;
+
+  pWnd->SetSelect(index);
+  return true;
+}
+
+bool CFFL_ComboBox::IsIndexSelected(int index) {
+  if (!IsValid())
+    return false;
+
+  if (index < 0 || index >= m_pWidget->CountOptions())
+    return false;
+
+  CPDFSDK_PageView* pPageView = GetCurPageView(true);
+  ASSERT(pPageView);
+
+  CPWL_ComboBox* pWnd =
+      static_cast<CPWL_ComboBox*>(GetPDFWindow(pPageView, false));
+  if (!pWnd)
+    return false;
+
+  return index == pWnd->GetSelect();
+}
+
 #ifdef PDF_ENABLE_XFA
 bool CFFL_ComboBox::IsFieldFull(CPDFSDK_PageView* pPageView) {
   if (CPWL_ComboBox* pComboBox =
diff --git a/fpdfsdk/formfiller/cffl_combobox.h b/fpdfsdk/formfiller/cffl_combobox.h
index e33130b..52eb874 100644
--- a/fpdfsdk/formfiller/cffl_combobox.h
+++ b/fpdfsdk/formfiller/cffl_combobox.h
@@ -46,6 +46,8 @@
                            const CPDFSDK_FieldAction& faNew) override;
   void SaveState(CPDFSDK_PageView* pPageView) override;
   void RestoreState(CPDFSDK_PageView* pPageView) override;
+  bool SetIndexSelected(int index, bool selected) override;
+  bool IsIndexSelected(int index) override;
 #ifdef PDF_ENABLE_XFA
   bool IsFieldFull(CPDFSDK_PageView* pPageView) override;
 #endif
diff --git a/fpdfsdk/formfiller/cffl_formfiller.cpp b/fpdfsdk/formfiller/cffl_formfiller.cpp
index 55d3304..7d25f1f 100644
--- a/fpdfsdk/formfiller/cffl_formfiller.cpp
+++ b/fpdfsdk/formfiller/cffl_formfiller.cpp
@@ -206,6 +206,14 @@
   return pWnd && pWnd->OnChar(nChar, nFlags);
 }
 
+bool CFFL_FormFiller::SetIndexSelected(int index, bool selected) {
+  return false;
+}
+
+bool CFFL_FormFiller::IsIndexSelected(int index) {
+  return false;
+}
+
 WideString CFFL_FormFiller::GetText(CPDFSDK_Annot* pAnnot) {
   if (!IsValid())
     return WideString();
diff --git a/fpdfsdk/formfiller/cffl_formfiller.h b/fpdfsdk/formfiller/cffl_formfiller.h
index 3bc5415..30e329f 100644
--- a/fpdfsdk/formfiller/cffl_formfiller.h
+++ b/fpdfsdk/formfiller/cffl_formfiller.h
@@ -75,6 +75,8 @@
                          uint32_t nKeyCode,
                          uint32_t nFlags);
   virtual bool OnChar(CPDFSDK_Annot* pAnnot, uint32_t nChar, uint32_t nFlags);
+  virtual bool SetIndexSelected(int index, bool selected);
+  virtual bool IsIndexSelected(int index);
 
   WideString GetText(CPDFSDK_Annot* pAnnot);
   WideString GetSelectedText(CPDFSDK_Annot* pAnnot);
diff --git a/fpdfsdk/formfiller/cffl_interactiveformfiller.cpp b/fpdfsdk/formfiller/cffl_interactiveformfiller.cpp
index bc95f4b..6e9f132 100644
--- a/fpdfsdk/formfiller/cffl_interactiveformfiller.cpp
+++ b/fpdfsdk/formfiller/cffl_interactiveformfiller.cpp
@@ -290,6 +290,25 @@
   return true;
 }
 
+bool CFFL_InteractiveFormFiller::SetIndexSelected(
+    CPDFSDK_Annot::ObservedPtr* pAnnot,
+    int index,
+    bool selected) {
+  ASSERT((*pAnnot)->GetPDFAnnot()->GetSubtype() == CPDF_Annot::Subtype::WIDGET);
+
+  CFFL_FormFiller* pFormFiller = GetFormFiller(pAnnot->Get(), false);
+  return pFormFiller && pFormFiller->SetIndexSelected(index, selected);
+}
+
+bool CFFL_InteractiveFormFiller::IsIndexSelected(
+    CPDFSDK_Annot::ObservedPtr* pAnnot,
+    int index) {
+  ASSERT((*pAnnot)->GetPDFAnnot()->GetSubtype() == CPDF_Annot::Subtype::WIDGET);
+
+  CFFL_FormFiller* pFormFiller = GetFormFiller(pAnnot->Get(), false);
+  return pFormFiller && pFormFiller->IsIndexSelected(index);
+}
+
 bool CFFL_InteractiveFormFiller::OnLButtonDblClk(
     CPDFSDK_PageView* pPageView,
     CPDFSDK_Annot::ObservedPtr* pAnnot,
diff --git a/fpdfsdk/formfiller/cffl_interactiveformfiller.h b/fpdfsdk/formfiller/cffl_interactiveformfiller.h
index f4c24e3..4e6fceb 100644
--- a/fpdfsdk/formfiller/cffl_interactiveformfiller.h
+++ b/fpdfsdk/formfiller/cffl_interactiveformfiller.h
@@ -111,6 +111,12 @@
   bool OnButtonUp(CPDFSDK_Annot::ObservedPtr* pAnnot,
                   CPDFSDK_PageView* pPageView,
                   uint32_t nFlag);
+
+  bool SetIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                        int index,
+                        bool selected);
+  bool IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot, int index);
+
 #ifdef PDF_ENABLE_XFA
   bool OnClick(CPDFSDK_Annot::ObservedPtr* pAnnot,
                CPDFSDK_PageView* pPageView,
diff --git a/fpdfsdk/formfiller/cffl_listbox.cpp b/fpdfsdk/formfiller/cffl_listbox.cpp
index d521be6..0955725 100644
--- a/fpdfsdk/formfiller/cffl_listbox.cpp
+++ b/fpdfsdk/formfiller/cffl_listbox.cpp
@@ -195,3 +195,47 @@
   for (const auto& item : m_State)
     pListBox->Select(item);
 }
+
+bool CFFL_ListBox::SetIndexSelected(int index, bool selected) {
+  if (!IsValid())
+    return false;
+
+  if (index < 0 || index >= m_pWidget->CountOptions())
+    return false;
+
+  CPDFSDK_PageView* pPageView = GetCurPageView(true);
+  ASSERT(pPageView);
+
+  CPWL_ListBox* pListBox =
+      static_cast<CPWL_ListBox*>(GetPDFWindow(pPageView, false));
+  if (!pListBox)
+    return false;
+
+  if (selected) {
+    pListBox->Select(index);
+    pListBox->SetCaret(index);
+  } else {
+    pListBox->Deselect(index);
+    pListBox->SetCaret(index);
+  }
+
+  return true;
+}
+
+bool CFFL_ListBox::IsIndexSelected(int index) {
+  if (!IsValid())
+    return false;
+
+  if (index < 0 || index >= m_pWidget->CountOptions())
+    return false;
+
+  CPDFSDK_PageView* pPageView = GetCurPageView(true);
+  ASSERT(pPageView);
+
+  CPWL_ListBox* pListBox =
+      static_cast<CPWL_ListBox*>(GetPDFWindow(pPageView, false));
+  if (!pListBox)
+    return false;
+
+  return pListBox->IsItemSelected(index);
+}
diff --git a/fpdfsdk/formfiller/cffl_listbox.h b/fpdfsdk/formfiller/cffl_listbox.h
index 7fbaf0c..3d71e7c 100644
--- a/fpdfsdk/formfiller/cffl_listbox.h
+++ b/fpdfsdk/formfiller/cffl_listbox.h
@@ -33,6 +33,8 @@
                      CPDFSDK_FieldAction& fa) override;
   void SaveState(CPDFSDK_PageView* pPageView) override;
   void RestoreState(CPDFSDK_PageView* pPageView) override;
+  bool SetIndexSelected(int index, bool selected) override;
+  bool IsIndexSelected(int index) override;
 
  private:
   std::set<int> m_OriginSelections;
diff --git a/fpdfsdk/fpdf_formfill.cpp b/fpdfsdk/fpdf_formfill.cpp
index b84d906..79ea68d 100644
--- a/fpdfsdk/fpdf_formfill.cpp
+++ b/fpdfsdk/fpdf_formfill.cpp
@@ -725,3 +725,22 @@
     pActionHandler->DoAction_Page(action, type, pFormFillEnv);
   }
 }
+
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FORM_SetIndexSelected(FPDF_FORMHANDLE hHandle,
+                      FPDF_PAGE page,
+                      int index,
+                      FPDF_BOOL selected) {
+  CPDFSDK_PageView* pPageView = FormHandleToPageView(hHandle, page);
+  if (!pPageView)
+    return false;
+  return pPageView->SetIndexSelected(index, selected);
+}
+
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FORM_IsIndexSelected(FPDF_FORMHANDLE hHandle, FPDF_PAGE page, int index) {
+  CPDFSDK_PageView* pPageView = FormHandleToPageView(hHandle, page);
+  if (!pPageView)
+    return false;
+  return pPageView->IsIndexSelected(index);
+}
diff --git a/fpdfsdk/fpdf_formfill_embeddertest.cpp b/fpdfsdk/fpdf_formfill_embeddertest.cpp
index fe36b7f..03924a3 100644
--- a/fpdfsdk/fpdf_formfill_embeddertest.cpp
+++ b/fpdfsdk/fpdf_formfill_embeddertest.cpp
@@ -153,6 +153,18 @@
 
   void PerformRedo() { EXPECT_TRUE(FORM_Redo(form_handle(), page_)); }
 
+  void SetIndexSelectedShouldSucceed(int index, bool selected) {
+    EXPECT_TRUE(FORM_SetIndexSelected(form_handle(), page_, index, selected));
+  }
+
+  void SetIndexSelectedShouldFail(int index, bool selected) {
+    EXPECT_FALSE(FORM_SetIndexSelected(form_handle(), page_, index, selected));
+  }
+
+  void CheckIsIndexSelected(int index, bool expected) {
+    EXPECT_EQ(expected, FORM_IsIndexSelected(form_handle(), page_, index));
+  }
+
  private:
   FPDF_PAGE page_ = nullptr;
 };
@@ -268,6 +280,14 @@
     SelectTextWithMouse(EditableFormEnd(), EditableFormBegin());
   }
 
+  void FocusOnEditableForm() { FocusOnPoint(EditableFormDropDown()); }
+
+  void FocusOnNonEditableForm() { FocusOnPoint(NonEditableFormDropDown()); }
+
+  void FocusOnPoint(const CFX_PointF& point) {
+    EXPECT_EQ(true, FORM_OnFocus(form_handle(), page(), 0, point.x, point.y));
+  }
+
   const CFX_PointF& EditableFormBegin() const {
     static const CFX_PointF point = EditableFormAtX(kFormBeginX);
     return point;
@@ -336,6 +356,97 @@
   static constexpr float kNonEditableFormY = 410.0;
 };
 
+class FPDFFormFillListBoxFormEmbedderTest
+    : public FPDFFormFillInteractiveEmbedderTest {
+ protected:
+  FPDFFormFillListBoxFormEmbedderTest() = default;
+  ~FPDFFormFillListBoxFormEmbedderTest() override = default;
+
+  const char* GetDocumentName() const override {
+    // PDF with form listboxes:
+    // - "Listbox_SingleSelect" - Ff: 0, 3 options with pair values.
+    // - "Listbox_MultiSelect" - Ff: 2097152, 26 options with single values.
+    // - "Listbox_ReadOnly" - Ff: 1, 3 options with single values.
+    return "listbox_form.pdf";
+  }
+
+  int GetFormType() const override { return FPDF_FORMFIELD_LISTBOX; }
+
+  void FormSanityChecks() override {
+    EXPECT_EQ(GetFormType(),
+              GetFormTypeAtPoint(SingleSelectFirstVisibleOption()));
+    EXPECT_EQ(GetFormType(),
+              GetFormTypeAtPoint(SingleSelectSecondVisibleOption()));
+    EXPECT_EQ(GetFormType(),
+              GetFormTypeAtPoint(MultiSelectFirstVisibleOption()));
+    EXPECT_EQ(GetFormType(),
+              GetFormTypeAtPoint(MultiSelectSecondVisibleOption()));
+  }
+
+  void ClickOnSingleSelectFormOption(int item_index) {
+    // Only the first two indices are visible so can only click on those
+    // without scrolling.
+    ASSERT(item_index >= 0);
+    ASSERT(item_index < 2);
+    if (item_index == 0) {
+      ClickOnFormFieldAtPoint(SingleSelectFirstVisibleOption());
+    } else {
+      ClickOnFormFieldAtPoint(SingleSelectSecondVisibleOption());
+    }
+  }
+
+  void ClickOnMultiSelectFormOption(int item_index) {
+    // Only the first two indices are visible so can only click on those
+    // without scrolling.
+    ASSERT(item_index >= 0);
+    ASSERT(item_index < 2);
+    if (item_index == 0) {
+      ClickOnFormFieldAtPoint(MultiSelectFirstVisibleOption());
+    } else {
+      ClickOnFormFieldAtPoint(MultiSelectSecondVisibleOption());
+    }
+  }
+
+  void FocusOnSingleSelectForm() {
+    FocusOnPoint(SingleSelectFirstVisibleOption());
+  }
+
+  void FocusOnMultiSelectForm() {
+    FocusOnPoint(MultiSelectFirstVisibleOption());
+  }
+
+  void FocusOnPoint(const CFX_PointF& point) {
+    EXPECT_EQ(true, FORM_OnFocus(form_handle(), page(), 0, point.x, point.y));
+  }
+
+  const CFX_PointF& SingleSelectFirstVisibleOption() const {
+    static const CFX_PointF point(kFormBeginX, kSingleFormYFirstVisibleOption);
+    return point;
+  }
+
+  const CFX_PointF& SingleSelectSecondVisibleOption() const {
+    static const CFX_PointF point(kFormBeginX, kSingleFormYSecondVisibleOption);
+    return point;
+  }
+
+  const CFX_PointF& MultiSelectFirstVisibleOption() const {
+    static const CFX_PointF point(kFormBeginX, kMultiFormYFirstVisibleOption);
+    return point;
+  }
+
+  const CFX_PointF& MultiSelectSecondVisibleOption() const {
+    static const CFX_PointF point(kFormBeginX, kMultiFormYSecondVisibleOption);
+    return point;
+  }
+
+ private:
+  static constexpr float kFormBeginX = 102.0;
+  static constexpr float kSingleFormYFirstVisibleOption = 371.0;
+  static constexpr float kSingleFormYSecondVisibleOption = 358.0;
+  static constexpr float kMultiFormYFirstVisibleOption = 423.0;
+  static constexpr float kMultiFormYSecondVisibleOption = 408.0;
+};
+
 TEST_F(FPDFFormFillEmbedderTest, FirstTest) {
   EmbedderTestMockDelegate mock;
   EXPECT_CALL(mock, Alert(_, _, _, _)).Times(0);
@@ -967,6 +1078,115 @@
   CheckFocusedFieldText(L"ABCDEFGHIJ");
 }
 
+TEST_F(FPDFFormFillComboBoxFormEmbedderTest,
+       SetSelectionProgrammaticallyNonEditableField) {
+  // Focus on non-editable form field and check that the value is as expected.
+  // This is the value that is present in the field upon opening, we have not
+  // changed it by setting focus.
+  FocusOnNonEditableForm();
+  CheckFocusedFieldText(L"Banana");
+
+  // Make selections to change the value of the focused annotation
+  // programmatically.
+  SetIndexSelectedShouldSucceed(0, true);
+  CheckFocusedFieldText(L"Apple");
+
+  // Selecting an index that is already selected is success.
+  SetIndexSelectedShouldSucceed(0, true);
+  CheckFocusedFieldText(L"Apple");
+
+  SetIndexSelectedShouldSucceed(9, true);
+  CheckFocusedFieldText(L"Jackfruit");
+
+  // Cannot deselect a combobox field - value unchanged.
+  SetIndexSelectedShouldFail(9, false);
+  CheckFocusedFieldText(L"Jackfruit");
+
+  // Cannot select indices that are out of range - value unchanged.
+  SetIndexSelectedShouldFail(100, true);
+  SetIndexSelectedShouldFail(-100, true);
+  CheckFocusedFieldText(L"Jackfruit");
+
+  // Check that above actions are interchangeable with click actions, should be
+  // able to use a combination of both.
+  SelectNonEditableFormOption(1);
+  CheckFocusedFieldText(L"Banana");
+}
+
+TEST_F(FPDFFormFillComboBoxFormEmbedderTest,
+       SetSelectionProgrammaticallyEditableField) {
+  // Focus on editable form field and check that the value is as expected.
+  // This is the value that is present in the field upon opening, we have not
+  // changed it by setting focus.
+  FocusOnEditableForm();
+  CheckFocusedFieldText(L"");
+
+  // Make selections to change value of the focused annotation
+  // programmatically.
+  SetIndexSelectedShouldSucceed(0, true);
+  CheckFocusedFieldText(L"Foo");
+
+  SetIndexSelectedShouldSucceed(1, true);
+  CheckFocusedFieldText(L"Bar");
+
+  // Selecting an index that is already selected is success.
+  SetIndexSelectedShouldSucceed(1, true);
+  CheckFocusedFieldText(L"Bar");
+
+  // Cannot deselect a combobox field - value unchanged.
+  SetIndexSelectedShouldFail(0, false);
+  CheckFocusedFieldText(L"Bar");
+
+  // Cannot select indices that are out of range - value unchanged.
+  SetIndexSelectedShouldFail(100, true);
+  SetIndexSelectedShouldFail(-100, true);
+  CheckFocusedFieldText(L"Bar");
+
+  // Check that above actions are interchangeable with click actions, should be
+  // able to use a combination of both.
+  SelectEditableFormOption(0);
+  CheckFocusedFieldText(L"Foo");
+
+  // Check that above actions are interchangeable with typing actions, should
+  // be able to use a combination of both. Typing text into a text field after
+  // selecting indices programmatically should be equivalent to doing so after
+  // a user selects an index via click on the dropdown.
+  SetIndexSelectedShouldSucceed(1, true);
+  CheckFocusedFieldText(L"Bar");
+  TypeTextIntoTextField(5, EditableFormBegin());
+  CheckFocusedFieldText(L"ABCDEBar");
+}
+
+TEST_F(FPDFFormFillComboBoxFormEmbedderTest,
+       CheckIfIndexSelectedNonEditableField) {
+  // Non-editable field is set to 'Banana' (index 1) upon opening.
+  ClickOnFormFieldAtPoint(NonEditableFormBegin());
+  for (int i = 0; i < 26; i++) {
+    bool expected = i == 1;
+    CheckIsIndexSelected(i, expected);
+  }
+
+  SelectNonEditableFormOption(0);
+  CheckIsIndexSelected(0, true);
+  for (int i = 1; i < 26; i++) {
+    CheckIsIndexSelected(i, false);
+  }
+}
+
+TEST_F(FPDFFormFillComboBoxFormEmbedderTest,
+       CheckIfIndexSelectedEditableField) {
+  // Editable field has nothing selected upon opening.
+  ClickOnFormFieldAtPoint(EditableFormBegin());
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, false);
+  CheckIsIndexSelected(2, false);
+
+  SelectEditableFormOption(0);
+  CheckIsIndexSelected(0, true);
+  CheckIsIndexSelected(1, false);
+  CheckIsIndexSelected(2, false);
+}
+
 TEST_F(FPDFFormFillTextFormEmbedderTest, DeleteTextFieldEntireSelection) {
   // Select entire contents of text field.
   TypeTextIntoTextField(12, RegularFormBegin());
@@ -1782,6 +2002,33 @@
   CheckCanRedo(false);
 }
 
+// This action only applies to Listboxes and Comboboxes so should fail
+// gracefully for Textboxes by returning false to all operations.
+TEST_F(FPDFFormFillTextFormEmbedderTest, SetIndexSelectedShouldFailGracefully) {
+  // set focus and read text to confirm we have it
+  ClickOnFormFieldAtPoint(CharLimitFormEnd());
+  CheckFocusedFieldText(L"Elephant");
+
+  SetIndexSelectedShouldFail(0, true);
+  SetIndexSelectedShouldFail(0, false);
+  SetIndexSelectedShouldFail(1, true);
+  SetIndexSelectedShouldFail(1, false);
+  SetIndexSelectedShouldFail(-1, true);
+  SetIndexSelectedShouldFail(-1, false);
+}
+
+// This action only applies to Listboxes and Comboboxes so should fail
+// gracefully for Textboxes by returning false to all operations.
+TEST_F(FPDFFormFillTextFormEmbedderTest, IsIndexSelectedShouldFailGracefully) {
+  // set focus and read text to confirm we have it
+  ClickOnFormFieldAtPoint(CharLimitFormEnd());
+  CheckFocusedFieldText(L"Elephant");
+
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, false);
+  CheckIsIndexSelected(-1, false);
+}
+
 TEST_F(FPDFFormFillComboBoxFormEmbedderTest, UndoRedo) {
   ClickOnFormFieldAtPoint(NonEditableFormBegin());
   CheckFocusedFieldText(L"Banana");
@@ -1816,3 +2063,171 @@
   CheckCanUndo(true);
   CheckCanRedo(true);
 }
+
+TEST_F(FPDFFormFillListBoxFormEmbedderTest,
+       CheckIfIndexSelectedSingleSelectField) {
+  // Nothing is selected in single select field upon opening.
+  FocusOnSingleSelectForm();
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, false);
+  CheckIsIndexSelected(2, false);
+
+  ClickOnSingleSelectFormOption(1);
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, true);
+  CheckIsIndexSelected(2, false);
+}
+
+TEST_F(FPDFFormFillListBoxFormEmbedderTest,
+       CheckIfIndexSelectedMultiSelectField) {
+  // Multiselect field set to 'Banana' (index 1) upon opening.
+  FocusOnMultiSelectForm();
+  for (int i = 0; i < 26; i++) {
+    bool expected = i == 1;
+    CheckIsIndexSelected(i, expected);
+  }
+
+  ClickOnMultiSelectFormOption(0);
+  for (int i = 0; i < 26; i++) {
+    bool expected = i == 0;
+    CheckIsIndexSelected(i, expected);
+  }
+}
+
+TEST_F(FPDFFormFillListBoxFormEmbedderTest,
+       SetSelectionProgrammaticallySingleSelectField) {
+  // Nothing is selected in single select field upon opening.
+  FocusOnSingleSelectForm();
+  CheckFocusedFieldText(L"");
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, false);
+  CheckIsIndexSelected(2, false);
+
+  // Make selections to change the value of the focused annotation
+  // programmatically showing that only one value remains selected at a time.
+  SetIndexSelectedShouldSucceed(0, true);
+  CheckFocusedFieldText(L"Foo");
+  CheckIsIndexSelected(0, true);
+  CheckIsIndexSelected(1, false);
+  CheckIsIndexSelected(2, false);
+
+  SetIndexSelectedShouldSucceed(1, true);
+  CheckFocusedFieldText(L"Bar");
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, true);
+  CheckIsIndexSelected(2, false);
+
+  // Selecting/deselecting an index that is already selected/deselected is
+  // success.
+  SetIndexSelectedShouldSucceed(1, true);
+  CheckFocusedFieldText(L"Bar");
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, true);
+  CheckIsIndexSelected(2, false);
+
+  SetIndexSelectedShouldSucceed(2, false);
+  CheckFocusedFieldText(L"Bar");
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, true);
+  CheckIsIndexSelected(2, false);
+
+  // Cannot select indices that are out of range.
+  SetIndexSelectedShouldFail(100, true);
+  SetIndexSelectedShouldFail(-100, true);
+  SetIndexSelectedShouldFail(100, false);
+  SetIndexSelectedShouldFail(-100, false);
+  // Confirm that previous values were not changed.
+  CheckFocusedFieldText(L"Bar");
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, true);
+  CheckIsIndexSelected(2, false);
+
+  // Unlike combobox, should be able to deselect all indices.
+  SetIndexSelectedShouldSucceed(1, false);
+  CheckFocusedFieldText(L"");
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, false);
+  CheckIsIndexSelected(2, false);
+
+  // Check that above actions are interchangeable with click actions, should be
+  // able to use a combination of both.
+  ClickOnSingleSelectFormOption(1);
+  CheckFocusedFieldText(L"Bar");
+  CheckIsIndexSelected(0, false);
+  CheckIsIndexSelected(1, true);
+  CheckIsIndexSelected(2, false);
+}
+
+// Re: Focus Field Text - For multiselect listboxes a caret is set on the last
+// item to be selected/deselected. The text of that item should be returned.
+TEST_F(FPDFFormFillListBoxFormEmbedderTest,
+       SetSelectionProgrammaticallyMultiSelectField) {
+  // Multiselect field set to 'Banana' (index 1) upon opening.
+  FocusOnMultiSelectForm();
+  for (int i = 0; i < 26; i++) {
+    bool expected = i == 1;
+    CheckIsIndexSelected(i, expected);
+  }
+  CheckFocusedFieldText(L"Banana");
+
+  // Select some more options.
+  SetIndexSelectedShouldSucceed(5, true);
+  SetIndexSelectedShouldSucceed(6, true);
+  SetIndexSelectedShouldSucceed(20, true);
+  for (int i = 0; i < 26; i++) {
+    bool expected = (i == 1 || i == 5 || i == 6 || i == 20);
+    CheckIsIndexSelected(i, expected);
+  }
+  CheckFocusedFieldText(L"Ugli Fruit");
+
+  // Selecting indices that are already selected is success - changes nothing.
+  SetIndexSelectedShouldSucceed(5, true);
+  SetIndexSelectedShouldSucceed(6, true);
+  SetIndexSelectedShouldSucceed(20, true);
+  for (int i = 0; i < 26; i++) {
+    bool expected = (i == 1 || i == 5 || i == 6 || i == 20);
+    CheckIsIndexSelected(i, expected);
+  }
+  CheckFocusedFieldText(L"Ugli Fruit");
+
+  // Deselect some options.
+  SetIndexSelectedShouldSucceed(20, false);
+  SetIndexSelectedShouldSucceed(1, false);
+  for (int i = 0; i < 26; i++) {
+    bool expected = (i == 5 || i == 6);
+    CheckIsIndexSelected(i, expected);
+  }
+  CheckFocusedFieldText(L"Banana");
+
+  // Deselecting indices that already aren't selected is success - does not
+  // change the selected values but moves the focus text caret to last item we
+  // executed on.
+  SetIndexSelectedShouldSucceed(1, false);
+  SetIndexSelectedShouldSucceed(3, false);
+  for (int i = 0; i < 26; i++) {
+    bool expected = (i == 5 || i == 6);
+    CheckIsIndexSelected(i, expected);
+  }
+  CheckFocusedFieldText(L"Date");
+
+  // Cannot select indices that are out of range.
+  SetIndexSelectedShouldFail(100, true);
+  SetIndexSelectedShouldFail(-100, true);
+  SetIndexSelectedShouldFail(100, false);
+  SetIndexSelectedShouldFail(-100, false);
+  // Confirm that previous values were not changed.
+  CheckFocusedFieldText(L"Date");
+  for (int i = 0; i < 26; i++) {
+    bool expected = (i == 5 || i == 6);
+    CheckIsIndexSelected(i, expected);
+  }
+
+  // Check that above actions are interchangeable with click actions, should be
+  // able to use a combination of both.
+  ClickOnMultiSelectFormOption(1);
+  for (int i = 0; i < 26; i++) {
+    bool expected = i == 1;
+    CheckIsIndexSelected(i, expected);
+  }
+  CheckFocusedFieldText(L"Banana");
+}
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index ad7599f..27069d7 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -238,6 +238,7 @@
     CHK(FORM_ForceToKillFocus);
     CHK(FORM_GetFocusedText);
     CHK(FORM_GetSelectedText);
+    CHK(FORM_IsIndexSelected);
     CHK(FORM_OnAfterLoadPage);
     CHK(FORM_OnBeforeClosePage);
     CHK(FORM_OnChar);
@@ -254,6 +255,7 @@
 #endif
     CHK(FORM_Redo);
     CHK(FORM_ReplaceSelection);
+    CHK(FORM_SetIndexSelected);
     CHK(FORM_Undo);
     CHK(FPDFDOC_ExitFormFillEnvironment);
     CHK(FPDFDOC_InitFormFillEnvironment);
diff --git a/fpdfsdk/ipdfsdk_annothandler.h b/fpdfsdk/ipdfsdk_annothandler.h
index 61c0ad0..c9f9e30 100644
--- a/fpdfsdk/ipdfsdk_annothandler.h
+++ b/fpdfsdk/ipdfsdk_annothandler.h
@@ -101,6 +101,13 @@
                           uint32_t nFlag) = 0;
   virtual bool OnKillFocus(CPDFSDK_Annot::ObservedPtr* pAnnot,
                            uint32_t nFlag) = 0;
+
+  virtual bool SetIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                                int index,
+                                bool selected) = 0;
+  virtual bool IsIndexSelected(CPDFSDK_Annot::ObservedPtr* pAnnot,
+                               int index) = 0;
+
 #ifdef PDF_ENABLE_XFA
   virtual bool OnXFAChangedFocus(CPDFSDK_Annot::ObservedPtr* pOldAnnot,
                                  CPDFSDK_Annot::ObservedPtr* pNewAnnot) = 0;
diff --git a/fpdfsdk/pwl/cpwl_list_box.cpp b/fpdfsdk/pwl/cpwl_list_box.cpp
index 827742b..2d2c439 100644
--- a/fpdfsdk/pwl/cpwl_list_box.cpp
+++ b/fpdfsdk/pwl/cpwl_list_box.cpp
@@ -302,6 +302,10 @@
   m_pList->Select(nItemIndex);
 }
 
+void CPWL_ListBox::Deselect(int32_t nItemIndex) {
+  m_pList->Deselect(nItemIndex);
+}
+
 void CPWL_ListBox::SetCaret(int32_t nItemIndex) {
   m_pList->SetCaret(nItemIndex);
 }
diff --git a/fpdfsdk/pwl/cpwl_list_box.h b/fpdfsdk/pwl/cpwl_list_box.h
index 2b4d693..ab53b70 100644
--- a/fpdfsdk/pwl/cpwl_list_box.h
+++ b/fpdfsdk/pwl/cpwl_list_box.h
@@ -72,6 +72,7 @@
   void ResetContent();
   void Reset();
   void Select(int32_t nItemIndex);
+  void Deselect(int32_t nItemIndex);
   void SetCaret(int32_t nItemIndex);
   void SetHoverSel(bool bHoverSel);
 
diff --git a/fpdfsdk/pwl/cpwl_list_impl.cpp b/fpdfsdk/pwl/cpwl_list_impl.cpp
index d8a6a1d..edcf138 100644
--- a/fpdfsdk/pwl/cpwl_list_impl.cpp
+++ b/fpdfsdk/pwl/cpwl_list_impl.cpp
@@ -407,6 +407,16 @@
   }
 }
 
+void CPWL_ListCtrl::Deselect(int32_t nItemIndex) {
+  if (!IsItemSelected(nItemIndex))
+    return;
+
+  SetMultipleSelect(nItemIndex, false);
+
+  if (!IsMultipleSel())
+    m_nSelItem = -1;
+}
+
 bool CPWL_ListCtrl::IsItemVisible(int32_t nItemIndex) const {
   CFX_FloatRect rcPlate = m_rcPlate;
   CFX_FloatRect rcItem = GetItemRect(nItemIndex);
diff --git a/fpdfsdk/pwl/cpwl_list_impl.h b/fpdfsdk/pwl/cpwl_list_impl.h
index e39a9c8..010793c 100644
--- a/fpdfsdk/pwl/cpwl_list_impl.h
+++ b/fpdfsdk/pwl/cpwl_list_impl.h
@@ -71,6 +71,7 @@
   void AddString(const WideString& str);
   void SetTopItem(int32_t nIndex);
   void Select(int32_t nItemIndex);
+  void Deselect(int32_t nItemIndex);
   void SetCaret(int32_t nItemIndex);
   void Empty();
   void Cancel();