Allowing focus to move across pages
The CL modifies CPDFSDK_PageView::OnKeyDown() to handle the key down
events that come when there is no focused annotation.
The CL also modifies CPDFSDK_AnnotIterator::GetNextAnnot() and
CPDFSDK_AnnotIterator::GetPrevAnnot() to prevent tab cycle within a PDF
page. The CL also includes tests to validate tabbing behavior for a
PDF page with form fields.
Bug: chromium:989040
Change-Id: Iace6eef4704a66cf0db20946f7b8fd15b5f679f1
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/65810
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 e5e0313..891e950 100644
--- a/fpdfsdk/cpdfsdk_annothandlermgr.cpp
+++ b/fpdfsdk/cpdfsdk_annothandlermgr.cpp
@@ -231,19 +231,35 @@
return GetAnnotHandler(pAnnot)->OnChar(pAnnot, nChar, nFlags);
}
-bool CPDFSDK_AnnotHandlerMgr::Annot_OnKeyDown(CPDFSDK_Annot* pAnnot,
+bool CPDFSDK_AnnotHandlerMgr::Annot_OnKeyDown(CPDFSDK_PageView* pPageView,
+ CPDFSDK_Annot* pAnnot,
int nKeyCode,
int nFlag) {
+ if (!pAnnot) {
+ // If pressed key is not tab then no action is needed.
+ if (nKeyCode != FWL_VKEY_Tab)
+ return false;
+
+ // If ctrl key or alt key is pressed, then no action is needed.
+ if (CPWL_Wnd::IsCTRLKeyDown(nFlag) || CPWL_Wnd::IsALTKeyDown(nFlag))
+ return false;
+
+ ObservedPtr<CPDFSDK_Annot> end_annot(GetFirstOrLastFocusableAnnot(
+ pPageView, CPWL_Wnd::IsSHIFTKeyDown(nFlag)));
+ return end_annot && pPageView->GetFormFillEnv()->SetFocusAnnot(&end_annot);
+ }
+
if (CPWL_Wnd::IsCTRLKeyDown(nFlag) || CPWL_Wnd::IsALTKeyDown(nFlag)) {
return GetAnnotHandler(pAnnot)->OnKeyDown(pAnnot, nKeyCode, nFlag);
}
ObservedPtr<CPDFSDK_Annot> pObservedAnnot(pAnnot);
- CPDFSDK_PageView* pPageView = pAnnot->GetPageView();
CPDFSDK_Annot* pFocusAnnot = pPageView->GetFocusAnnot();
if (pFocusAnnot && (nKeyCode == FWL_VKEY_Tab)) {
ObservedPtr<CPDFSDK_Annot> pNext(
GetNextAnnot(pFocusAnnot, !CPWL_Wnd::IsSHIFTKeyDown(nFlag)));
- if (pNext && pNext.Get() != pFocusAnnot) {
+ if (!pNext)
+ return false;
+ if (pNext.Get() != pFocusAnnot) {
pPageView->GetFormFillEnv()->SetFocusAnnot(&pNext);
return true;
}
@@ -333,3 +349,20 @@
pFormFillEnv->GetFocusableAnnotSubtypes());
return bNext ? ai.GetNextAnnot(pSDKAnnot) : ai.GetPrevAnnot(pSDKAnnot);
}
+
+CPDFSDK_Annot* CPDFSDK_AnnotHandlerMgr::GetFirstOrLastFocusableAnnot(
+ CPDFSDK_PageView* page_view,
+ bool last) const {
+#ifdef PDF_ENABLE_XFA
+ IPDF_Page* page = page_view->GetXFAPage();
+ if (page && !page->AsPDFPage()) {
+ // For XFA annots in XFA pages not backed by PDF pages.
+ return page->AsXFAPage()->GetFirstOrLastXFAAnnot(page_view, last);
+ }
+#endif // PDF_ENABLE_XFA
+
+ CPDFSDK_FormFillEnvironment* form_fill_env = page_view->GetFormFillEnv();
+ CPDFSDK_AnnotIterator ai(page_view,
+ form_fill_env->GetFocusableAnnotSubtypes());
+ return last ? ai.GetLastAnnot() : ai.GetFirstAnnot();
+}
diff --git a/fpdfsdk/cpdfsdk_annothandlermgr.h b/fpdfsdk/cpdfsdk_annothandlermgr.h
index cc3f715..a103469 100644
--- a/fpdfsdk/cpdfsdk_annothandlermgr.h
+++ b/fpdfsdk/cpdfsdk_annothandlermgr.h
@@ -97,7 +97,10 @@
uint32_t nFlags,
const CFX_PointF& point);
bool Annot_OnChar(CPDFSDK_Annot* pAnnot, uint32_t nChar, uint32_t nFlags);
- bool Annot_OnKeyDown(CPDFSDK_Annot* pAnnot, int nKeyCode, int nFlag);
+ bool Annot_OnKeyDown(CPDFSDK_PageView* pPageView,
+ CPDFSDK_Annot* pAnnot,
+ int nKeyCode,
+ int nFlag);
bool Annot_OnSetFocus(ObservedPtr<CPDFSDK_Annot>* pAnnot, uint32_t nFlag);
bool Annot_OnKillFocus(ObservedPtr<CPDFSDK_Annot>* pAnnot, uint32_t nFlag);
bool Annot_SetIndexSelected(ObservedPtr<CPDFSDK_Annot>* pAnnot,
@@ -123,6 +126,8 @@
IPDFSDK_AnnotHandler* GetAnnotHandlerOfType(
CPDF_Annot::Subtype nAnnotSubtype) const;
CPDFSDK_Annot* GetNextAnnot(CPDFSDK_Annot* pSDKAnnot, bool bNext);
+ CPDFSDK_Annot* GetFirstOrLastFocusableAnnot(CPDFSDK_PageView* page_view,
+ bool last) const;
// |m_pBAAnnotHandler| and |m_pWidgetHandler| are always present, but
// |m_pXFAWidgetHandler| is only present in XFA mode.
diff --git a/fpdfsdk/cpdfsdk_annotiterator.cpp b/fpdfsdk/cpdfsdk_annotiterator.cpp
index c1db6bb..1e0867a 100644
--- a/fpdfsdk/cpdfsdk_annotiterator.cpp
+++ b/fpdfsdk/cpdfsdk_annotiterator.cpp
@@ -70,16 +70,14 @@
return nullptr;
++iter;
if (iter == m_Annots.end())
- iter = m_Annots.begin();
+ return nullptr;
return *iter;
}
CPDFSDK_Annot* CPDFSDK_AnnotIterator::GetPrevAnnot(CPDFSDK_Annot* pAnnot) {
auto iter = std::find(m_Annots.begin(), m_Annots.end(), pAnnot);
- if (iter == m_Annots.end())
+ if (iter == m_Annots.begin() || iter == m_Annots.end())
return nullptr;
- if (iter == m_Annots.begin())
- iter = m_Annots.end();
return *(--iter);
}
diff --git a/fpdfsdk/cpdfsdk_annotiterator_embeddertest.cpp b/fpdfsdk/cpdfsdk_annotiterator_embeddertest.cpp
index 651e46b..556eb8b 100644
--- a/fpdfsdk/cpdfsdk_annotiterator_embeddertest.cpp
+++ b/fpdfsdk/cpdfsdk_annotiterator_embeddertest.cpp
@@ -55,7 +55,7 @@
pAnnot = iter.GetNextAnnot(pAnnot);
CheckRect(pAnnot->GetRect(), LeftBottom);
pAnnot = iter.GetNextAnnot(pAnnot);
- EXPECT_EQ(iter.GetFirstAnnot(), pAnnot);
+ EXPECT_EQ(nullptr, pAnnot);
pAnnot = iter.GetLastAnnot();
CheckRect(pAnnot->GetRect(), LeftBottom);
@@ -66,7 +66,7 @@
pAnnot = iter.GetPrevAnnot(pAnnot);
CheckRect(pAnnot->GetRect(), RightTop);
pAnnot = iter.GetPrevAnnot(pAnnot);
- EXPECT_EQ(iter.GetLastAnnot(), pAnnot);
+ EXPECT_EQ(nullptr, pAnnot);
}
{
// Page 1 specifies "column order"
@@ -81,7 +81,7 @@
pAnnot = iter.GetNextAnnot(pAnnot);
CheckRect(pAnnot->GetRect(), LeftBottom);
pAnnot = iter.GetNextAnnot(pAnnot);
- EXPECT_EQ(iter.GetFirstAnnot(), pAnnot);
+ EXPECT_EQ(nullptr, pAnnot);
pAnnot = iter.GetLastAnnot();
CheckRect(pAnnot->GetRect(), LeftBottom);
@@ -92,7 +92,7 @@
pAnnot = iter.GetPrevAnnot(pAnnot);
CheckRect(pAnnot->GetRect(), RightTop);
pAnnot = iter.GetPrevAnnot(pAnnot);
- EXPECT_EQ(iter.GetLastAnnot(), pAnnot);
+ EXPECT_EQ(nullptr, pAnnot);
}
{
// Page 2 specifies "struct order"
@@ -107,7 +107,7 @@
pAnnot = iter.GetNextAnnot(pAnnot);
CheckRect(pAnnot->GetRect(), RightBottom);
pAnnot = iter.GetNextAnnot(pAnnot);
- EXPECT_EQ(iter.GetFirstAnnot(), pAnnot);
+ EXPECT_EQ(nullptr, pAnnot);
pAnnot = iter.GetLastAnnot();
CheckRect(pAnnot->GetRect(), RightBottom);
@@ -118,7 +118,7 @@
pAnnot = iter.GetPrevAnnot(pAnnot);
CheckRect(pAnnot->GetRect(), LeftBottom);
pAnnot = iter.GetPrevAnnot(pAnnot);
- EXPECT_EQ(iter.GetLastAnnot(), pAnnot);
+ EXPECT_EQ(nullptr, pAnnot);
}
UnloadPage(page2);
UnloadPage(page1);
diff --git a/fpdfsdk/cpdfsdk_pageview.cpp b/fpdfsdk/cpdfsdk_pageview.cpp
index 45b3ffa..e63fca6 100644
--- a/fpdfsdk/cpdfsdk_pageview.cpp
+++ b/fpdfsdk/cpdfsdk_pageview.cpp
@@ -467,12 +467,10 @@
}
bool CPDFSDK_PageView::OnKeyDown(int nKeyCode, int nFlag) {
- if (CPDFSDK_Annot* pAnnot = GetFocusAnnot()) {
- CPDFSDK_AnnotHandlerMgr* pAnnotHandlerMgr =
- m_pFormFillEnv->GetAnnotHandlerMgr();
- return pAnnotHandlerMgr->Annot_OnKeyDown(pAnnot, nKeyCode, nFlag);
- }
- return false;
+ CPDFSDK_AnnotHandlerMgr* pAnnotHandlerMgr =
+ m_pFormFillEnv->GetAnnotHandlerMgr();
+ return pAnnotHandlerMgr->Annot_OnKeyDown(this, GetFocusAnnot(), nKeyCode,
+ nFlag);
}
bool CPDFSDK_PageView::OnKeyUp(int nKeyCode, int nFlag) {
diff --git a/fpdfsdk/fpdf_formfill_embeddertest.cpp b/fpdfsdk/fpdf_formfill_embeddertest.cpp
index af0df63..d9c33fe 100644
--- a/fpdfsdk/fpdf_formfill_embeddertest.cpp
+++ b/fpdfsdk/fpdf_formfill_embeddertest.cpp
@@ -727,6 +727,181 @@
UnloadPage(page);
}
+TEST_F(FPDFFormFillEmbedderTest, FormFillFirstTab) {
+ ASSERT_TRUE(OpenDocument("annotiter.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ // Invoking first tab on the page.
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, 0));
+ int page_index = -2;
+ FPDF_ANNOTATION annot = nullptr;
+ EXPECT_TRUE(FORM_GetFocusedAnnot(form_handle(), &page_index, &annot));
+ EXPECT_EQ(0, page_index);
+ ASSERT_TRUE(annot);
+ EXPECT_EQ(1, FPDFPage_GetAnnotIndex(page, annot));
+ FPDFPage_CloseAnnot(annot);
+
+ UnloadPage(page);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, FormFillFirstShiftTab) {
+ ASSERT_TRUE(OpenDocument("annotiter.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ // Invoking first shift-tab on the page.
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ShiftKey));
+ int page_index = -2;
+ FPDF_ANNOTATION annot = nullptr;
+ EXPECT_TRUE(FORM_GetFocusedAnnot(form_handle(), &page_index, &annot));
+ EXPECT_EQ(0, page_index);
+ ASSERT_TRUE(annot);
+ EXPECT_EQ(0, FPDFPage_GetAnnotIndex(page, annot));
+ FPDFPage_CloseAnnot(annot);
+
+ UnloadPage(page);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, FormFillContinuousTab) {
+ ASSERT_TRUE(OpenDocument("annotiter.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ static constexpr int kExpectedAnnotIndex[] = {1, 2, 3, 0};
+ // Tabs should iterate focus over annotations.
+ for (size_t i = 0; i < FX_ArraySize(kExpectedAnnotIndex); ++i) {
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, 0));
+ int page_index = -2;
+ FPDF_ANNOTATION annot = nullptr;
+ EXPECT_TRUE(FORM_GetFocusedAnnot(form_handle(), &page_index, &annot));
+ EXPECT_EQ(0, page_index);
+ ASSERT_TRUE(annot);
+ EXPECT_EQ(kExpectedAnnotIndex[i], FPDFPage_GetAnnotIndex(page, annot));
+ FPDFPage_CloseAnnot(annot);
+ }
+
+ // Tab should not be handled as the last annotation of the page is in focus.
+ ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, 0));
+
+ UnloadPage(page);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, FormFillContinuousShiftTab) {
+ ASSERT_TRUE(OpenDocument("annotiter.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ static constexpr int kExpectedAnnotIndex[] = {0, 3, 2, 1};
+ // Shift-tabs should iterate focus over annotations.
+ for (size_t i = 0; i < FX_ArraySize(kExpectedAnnotIndex); ++i) {
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ShiftKey));
+ int page_index = -2;
+ FPDF_ANNOTATION annot = nullptr;
+ EXPECT_TRUE(FORM_GetFocusedAnnot(form_handle(), &page_index, &annot));
+ EXPECT_EQ(0, page_index);
+ ASSERT_TRUE(annot);
+ EXPECT_EQ(kExpectedAnnotIndex[i], FPDFPage_GetAnnotIndex(page, annot));
+ FPDFPage_CloseAnnot(annot);
+ }
+
+ // Shift-tab should not be handled as the first annotation of the page is in
+ // focus.
+ ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ShiftKey));
+
+ UnloadPage(page);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, TabWithModifiers) {
+ EXPECT_TRUE(OpenDocument("annotiter.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ControlKey));
+
+ ASSERT_FALSE(
+ FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, FWL_EVENTFLAG_AltKey));
+
+ ASSERT_FALSE(
+ FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ (FWL_EVENTFLAG_ControlKey | FWL_EVENTFLAG_ShiftKey)));
+
+ ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ (FWL_EVENTFLAG_AltKey | FWL_EVENTFLAG_ShiftKey)));
+
+ UnloadPage(page);
+}
+
+#ifdef PDF_ENABLE_XFA
+TEST_F(FPDFFormFillEmbedderTest, XFAFormFillFirstTab) {
+ EXPECT_TRUE(OpenDocument("xfa/email_recommended.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ // Invoking first tab on the page.
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, 0));
+
+ UnloadPage(page);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, XFAFormFillFirstShiftTab) {
+ EXPECT_TRUE(OpenDocument("xfa/email_recommended.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ // Invoking first shift-tab on the page.
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ShiftKey));
+
+ UnloadPage(page);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, XFAFormFillContinuousTab) {
+ EXPECT_TRUE(OpenDocument("xfa/email_recommended.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ // Invoking first tab on the page.
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, 0));
+
+ // Subsequent tabs should move focus over annotations.
+ for (size_t i = 0; i < 9; ++i)
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, 0));
+
+ // Tab should not be handled as the last annotation of the page is in focus.
+ ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab, 0));
+
+ UnloadPage(page);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, XFAFormFillContinuousShiftTab) {
+ EXPECT_TRUE(OpenDocument("xfa/email_recommended.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ // Invoking first shift-tab on the page.
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ShiftKey));
+
+ // Subsequent shift-tabs should move focus over annotations.
+ for (size_t i = 0; i < 9; ++i) {
+ ASSERT_TRUE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ShiftKey));
+ }
+
+ // Shift-tab should not be handled as the first annotation of the page is in
+ // focus.
+ ASSERT_FALSE(FORM_OnKeyDown(form_handle(), page, FWL_VKEY_Tab,
+ FWL_EVENTFLAG_ShiftKey));
+
+ UnloadPage(page);
+}
+#endif // PDF_ENABLE_XFA
+
class DoURIActionBlockedDelegate final : public EmbedderTest::Delegate {
public:
void DoURIAction(FPDF_BYTESTRING uri) override {
diff --git a/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp b/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
index 9797429..d9868ab 100644
--- a/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
+++ b/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
@@ -196,12 +196,26 @@
CXFA_FFWidget* hNextFocus =
bNext ? pWidgetIterator->MoveToNext() : pWidgetIterator->MoveToPrevious();
- if (!hNextFocus && pSDKAnnot)
- hNextFocus = pWidgetIterator->MoveToFirst();
+ if (!hNextFocus)
+ return nullptr;
return pPageView->GetAnnotByXFAWidget(hNextFocus);
}
+CPDFSDK_Annot* CPDFXFA_Page::GetFirstOrLastXFAAnnot(CPDFSDK_PageView* page_view,
+ bool last) const {
+ CXFA_FFPageView* xfa_page_view = GetXFAPageView();
+ if (!xfa_page_view)
+ return nullptr;
+ std::unique_ptr<IXFA_WidgetIterator> it =
+ xfa_page_view->CreateTraverseWidgetIterator(XFA_WidgetStatus_Visible |
+ XFA_WidgetStatus_Viewable |
+ XFA_WidgetStatus_Focused);
+
+ return page_view->GetAnnotByXFAWidget(last ? it->MoveToLast()
+ : it->MoveToFirst());
+}
+
int CPDFXFA_Page::HasFormFieldAtPoint(const CFX_PointF& point) const {
CXFA_FFPageView* pPageView = GetXFAPageView();
if (!pPageView)
diff --git a/fpdfsdk/fpdfxfa/cpdfxfa_page.h b/fpdfsdk/fpdfxfa/cpdfxfa_page.h
index 73d5421..c121bab 100644
--- a/fpdfsdk/fpdfxfa/cpdfxfa_page.h
+++ b/fpdfsdk/fpdfxfa/cpdfxfa_page.h
@@ -19,6 +19,7 @@
class CPDF_Dictionary;
class CPDF_Document;
class CPDFSDK_Annot;
+class CPDFSDK_PageView;
class CXFA_FFPageView;
class CPDFXFA_Page final : public IPDF_Page {
@@ -48,6 +49,8 @@
void SetXFAPageViewIndex(int index) { m_iPageIndex = index; }
CXFA_FFPageView* GetXFAPageView() const;
CPDFSDK_Annot* GetNextXFAAnnot(CPDFSDK_Annot* pSDKAnnot, bool bNext);
+ CPDFSDK_Annot* GetFirstOrLastXFAAnnot(CPDFSDK_PageView* page_view,
+ bool last) const;
int HasFormFieldAtPoint(const CFX_PointF& point) const;
void DrawFocusAnnot(CFX_RenderDevice* pDevice,
CPDFSDK_Annot* pAnnot,