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();
diff --git a/public/fpdf_formfill.h b/public/fpdf_formfill.h
index fe04707..4bedfb5 100644
--- a/public/fpdf_formfill.h
+++ b/public/fpdf_formfill.h
@@ -1774,6 +1774,57 @@
**/
FPDF_EXPORT int FPDF_CALLCONV FPDF_GetFormType(FPDF_DOCUMENT document);
+/**
+ * Experimental API
+ * Function: FORM_SetIndexSelected
+ * Selects/deselects the value at the given |index| of the focused
+ * annotation.
+ * Parameters:
+ * hHandle - Handle to the form fill module. Returned by
+ * FPDFDOC_InitFormFillEnvironment.
+ * page - Handle to the page. Returned by FPDF_LoadPage
+ * index - 0-based index of value to be set as
+ * selected/unselected
+ * selected - true to select, false to deselect
+ * Return Value:
+ * TRUE if the operation succeeded.
+ * FALSE if the operation failed or widget is not a supported type.
+ * Comments:
+ * Intended for use with listbox/combobox widget types. Comboboxes
+ * have at most a single value selected at a time which cannot be
+ * deselected. Deselect on a combobox is a no-op that returns false.
+ * Default implementation is a no-op that will return false for
+ * other types.
+ * Not currently supported for XFA forms - will return false.
+ **/
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FORM_SetIndexSelected(FPDF_FORMHANDLE hHandle,
+ FPDF_PAGE page,
+ int index,
+ FPDF_BOOL selected);
+
+/**
+ * Experimental API
+ * Function: FORM_IsIndexSelected
+ * Returns whether or not the value at |index| of the focused
+ * annotation is currently selected.
+ * Parameters:
+ * hHandle - Handle to the form fill module. Returned by
+ * FPDFDOC_InitFormFillEnvironment.
+ * page - Handle to the page. Returned by FPDF_LoadPage
+ * index - 0-based Index of value to check
+ * Return Value:
+ * TRUE if value at |index| is currently selected.
+ * FALSE if value at |index| is not selected or widget is not a
+ * supported type.
+ * Comments:
+ * Intended for use with listbox/combobox widget types. Default
+ * implementation is a no-op that will return false for other types.
+ * Not currently supported for XFA forms - will return false.
+ **/
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FORM_IsIndexSelected(FPDF_FORMHANDLE hHandle, FPDF_PAGE page, int index);
+
#ifdef PDF_ENABLE_XFA
/**
* Function: FPDF_LoadXFA
diff --git a/testing/resources/listbox_form.in b/testing/resources/listbox_form.in
new file mode 100644
index 0000000..03acd56
--- /dev/null
+++ b/testing/resources/listbox_form.in
@@ -0,0 +1,89 @@
+{{header}}
+{{object 1 0}}
+<<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm << /Fields [ 8 0 R 9 0 R 10 0 R ] /DR 4 0 R >>
+>>
+endobj
+{{object 2 0}}
+<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
+endobj
+{{object 3 0}}
+<<
+ /Type /Page
+ /Parent 2 0 R
+ /Resources 4 0 R
+ /MediaBox [ 0 0 300 600 ]
+ /Contents 7 0 R
+ /Annots [ 8 0 R 9 0 R 10 0 R ]
+>>
+endobj
+{{object 4 0}}
+<< /Font 5 0 R >>
+endobj
+{{object 5 0}}
+<< /F1 6 0 R >>
+endobj
+{{object 6 0}}
+<<
+ /Type /Font
+ /Subtype /Type1
+ /BaseFont /Helvetica
+>>
+endobj
+{{object 7 0}}
+<< {{streamlen}} >>
+stream
+BT
+0 0 0 rg
+/F1 12 Tf
+100 450 Td
+(Test Form) Tj
+ET
+endstream
+endobj
+{{object 8 0}}
+<<
+ /Type /Annot
+ /FT /Ch
+ /Ff 0
+ /T (Listbox_SingleSelect)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 350 200 380 ]
+ /Subtype /Widget
+ /Opt [[(foo) (Foo)] [(bar) (Bar)] [(qux) (Qux)]]
+>>
+endobj
+{{object 9 0}}
+<<
+ /Type /Annot
+ /FT /Ch
+ /Ff 2097152
+ /T (Listbox_MultiSelect)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 400 200 430 ]
+ /Subtype /Widget
+ /Opt [(Apple) (Banana) (Cherry) (Date) (Elderberry) (Fig) (Guava) (Honeydew)
+ (Indian Fig) (Jackfruit) (Kiwi) (Lemon) (Mango) (Nectarine) (Orange)
+ (Persimmon) (Quince) (Raspberry) (Strawberry) (Tamarind) (Ugli Fruit)
+ (Voavanga) (Wolfberry) (Xigua) (Yangmei) (Zucchini)]
+ /V (Banana)
+>>
+endobj
+{{object 10 0}}
+<<
+ /Type /Annot
+ /FT /Ch
+ /Ff 1
+ /T (Listbox_ReadOnly)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 500 200 530 ]
+ /Subtype /Widget
+ /Opt [(Dog) (Elephant) (Frog)]
+>>
+endobj
+{{xref}}
+{{trailer}}
+{{startxref}}
+%%EOF
diff --git a/testing/resources/listbox_form.pdf b/testing/resources/listbox_form.pdf
new file mode 100644
index 0000000..7c244d1
--- /dev/null
+++ b/testing/resources/listbox_form.pdf
@@ -0,0 +1,106 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj
+<<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm << /Fields [ 8 0 R 9 0 R 10 0 R ] /DR 4 0 R >>
+>>
+endobj
+2 0 obj
+<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
+endobj
+3 0 obj
+<<
+ /Type /Page
+ /Parent 2 0 R
+ /Resources 4 0 R
+ /MediaBox [ 0 0 300 600 ]
+ /Contents 7 0 R
+ /Annots [ 8 0 R 9 0 R 10 0 R ]
+>>
+endobj
+4 0 obj
+<< /Font 5 0 R >>
+endobj
+5 0 obj
+<< /F1 6 0 R >>
+endobj
+6 0 obj
+<<
+ /Type /Font
+ /Subtype /Type1
+ /BaseFont /Helvetica
+>>
+endobj
+7 0 obj
+<< /Length 51 >>
+stream
+BT
+0 0 0 rg
+/F1 12 Tf
+100 450 Td
+(Test Form) Tj
+ET
+endstream
+endobj
+8 0 obj
+<<
+ /Type /Annot
+ /FT /Ch
+ /Ff 0
+ /T (Listbox_SingleSelect)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 350 200 380 ]
+ /Subtype /Widget
+ /Opt [[(foo) (Foo)] [(bar) (Bar)] [(qux) (Qux)]]
+>>
+endobj
+9 0 obj
+<<
+ /Type /Annot
+ /FT /Ch
+ /Ff 2097152
+ /T (Listbox_MultiSelect)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 400 200 430 ]
+ /Subtype /Widget
+ /Opt [(Apple) (Banana) (Cherry) (Date) (Elderberry) (Fig) (Guava) (Honeydew)
+ (Indian Fig) (Jackfruit) (Kiwi) (Lemon) (Mango) (Nectarine) (Orange)
+ (Persimmon) (Quince) (Raspberry) (Strawberry) (Tamarind) (Ugli Fruit)
+ (Voavanga) (Wolfberry) (Xigua) (Yangmei) (Zucchini)]
+ /V (Banana)
+>>
+endobj
+10 0 obj
+<<
+ /Type /Annot
+ /FT /Ch
+ /Ff 1
+ /T (Listbox_ReadOnly)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 500 200 530 ]
+ /Subtype /Widget
+ /Opt [(Dog) (Elephant) (Frog)]
+>>
+endobj
+xref
+0 11
+0000000000 65535 f
+0000000015 00000 n
+0000000127 00000 n
+0000000186 00000 n
+0000000335 00000 n
+0000000368 00000 n
+0000000399 00000 n
+0000000475 00000 n
+0000000575 00000 n
+0000000782 00000 n
+0000001252 00000 n
+trailer <<
+ /Root 1 0 R
+ /Size 11
+>>
+startxref
+1438
+%%EOF