0

Allow focus to move across pages on tab events

This CL adds tab handling in PDF. A tab event now allows the user to
move from the last annotation on a page to first annotation on the next
page.

Unit tests have been added to validate the scenario.

Bug: 989040
Change-Id: I46106e88946bdc3e667c9897cbcba3ff418aecfc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2061634
Commit-Queue: Ankit Kumar 🌪️ <ankk@microsoft.com>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Kevin Babbitt <kbabbitt@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#758074}
This commit is contained in:
Ankit Kumar
2020-04-09 23:35:34 +00:00
committed by Commit Bot
parent 2e5dbee4d9
commit 232f615092
3 changed files with 330 additions and 6 deletions

@ -37,6 +37,7 @@
#include "pdf/pdfium/pdfium_permissions.h"
#include "pdf/pdfium/pdfium_unsupported_features.h"
#include "pdf/url_loader_wrapper_impl.h"
#include "ppapi/c/ppb_input_event.h"
#include "ppapi/cpp/instance.h"
#include "ppapi/cpp/private/pdf.h"
#include "ppapi/cpp/var_dictionary.h"
@ -46,6 +47,7 @@
#include "third_party/pdfium/public/fpdf_attachment.h"
#include "third_party/pdfium/public/fpdf_catalog.h"
#include "third_party/pdfium/public/fpdf_ext.h"
#include "third_party/pdfium/public/fpdf_fwlevent.h"
#include "third_party/pdfium/public/fpdf_ppo.h"
#include "third_party/pdfium/public/fpdf_searchex.h"
#include "ui/events/keycodes/keyboard_codes.h"
@ -1056,6 +1058,7 @@ bool PDFiumEngine::OnLeftMouseDown(const pp::MouseInputEvent& event) {
return true;
if (page_index != -1) {
focus_item_type_ = FocusElementType::kPage;
last_focused_page_ = page_index;
double page_x;
double page_y;
@ -1479,6 +1482,11 @@ bool PDFiumEngine::ExtendSelection(int page_index, int char_index) {
}
bool PDFiumEngine::OnKeyDown(const pp::KeyboardInputEvent& event) {
// Handle tab events first as we might need to transition focus to an
// annotation in PDF.
if (event.GetKeyCode() == FWL_VKEY_Tab)
return HandleTabEvent(event.GetModifiers());
if (last_focused_page_ == -1)
return false;
@ -2689,12 +2697,6 @@ void PDFiumEngine::CalculateVisiblePages() {
} else {
pages_[i]->Unload();
}
// If the last focused page was a page that's no longer visible, reset
// that variable so that we don't send keyboard events to it (the focus
// will be lost when the page is first closed anyways).
if (static_cast<int>(i) == last_focused_page_)
last_focused_page_ = -1;
}
}
@ -3753,6 +3755,109 @@ PdfVersion PDFiumEngine::GetDocumentVersion() const {
}
}
bool PDFiumEngine::HandleTabEvent(uint32_t modifiers) {
bool alt_key = !!(modifiers & PP_INPUTEVENT_MODIFIER_ALTKEY);
bool ctrl_key = !!(modifiers & PP_INPUTEVENT_MODIFIER_CONTROLKEY);
if (alt_key || ctrl_key)
return HandleTabEventWithModifiers(modifiers);
return modifiers & PP_INPUTEVENT_MODIFIER_SHIFTKEY
? HandleTabBackward(modifiers)
: HandleTabForward(modifiers);
}
bool PDFiumEngine::HandleTabEventWithModifiers(uint32_t modifiers) {
// Only handle cases when a page is focused, else return false.
switch (focus_item_type_) {
case FocusElementType::kNone:
case FocusElementType::kDocument:
return false;
case FocusElementType::kPage:
if (last_focused_page_ == -1)
return false;
return !!FORM_OnKeyDown(form(), pages_[last_focused_page_]->GetPage(),
FWL_VKEY_Tab, modifiers);
default:
NOTREACHED();
return false;
}
}
bool PDFiumEngine::HandleTabForward(uint32_t modifiers) {
if (focus_item_type_ == FocusElementType::kNone) {
focus_item_type_ = FocusElementType::kDocument;
return true;
}
int page_index = last_focused_page_;
if (page_index == -1)
page_index = 0;
bool did_tab_forward = false;
while (!did_tab_forward && PageIndexInBounds(page_index)) {
did_tab_forward = !!FORM_OnKeyDown(form(), pages_[page_index]->GetPage(),
FWL_VKEY_Tab, modifiers);
if (!did_tab_forward)
++page_index;
}
if (did_tab_forward) {
last_focused_page_ = page_index;
focus_item_type_ = FocusElementType::kPage;
} else {
last_focused_page_ = -1;
focus_item_type_ = FocusElementType::kNone;
}
return did_tab_forward;
}
bool PDFiumEngine::HandleTabBackward(uint32_t modifiers) {
if (focus_item_type_ == FocusElementType::kDocument) {
focus_item_type_ = FocusElementType::kNone;
return false;
}
int page_index = last_focused_page_;
if (page_index == -1)
page_index = GetNumberOfPages() - 1;
bool did_tab_backward = false;
while (!did_tab_backward && PageIndexInBounds(page_index)) {
did_tab_backward = !!FORM_OnKeyDown(form(), pages_[page_index]->GetPage(),
FWL_VKEY_Tab, modifiers);
if (!did_tab_backward)
--page_index;
}
if (did_tab_backward) {
last_focused_page_ = page_index;
focus_item_type_ = FocusElementType::kPage;
} else {
// No focusable annotation found in pages. Possible scenarios:
// Case 1: |focus_item_type_| is None. Since no object in any page can take
// the focus, the document should take focus.
// Case 2: |focus_item_type_| is Page. Since there aren't any objects that
// could take focus, the document should take focus.
// Case 3: |focus_item_type_| is Document. Move focus_item_type_ to None.
switch (focus_item_type_) {
case FocusElementType::kPage:
case FocusElementType::kNone:
did_tab_backward = true;
last_focused_page_ = -1;
focus_item_type_ = FocusElementType::kDocument;
KillFormFocus();
break;
case FocusElementType::kDocument:
focus_item_type_ = FocusElementType::kNone;
break;
default:
NOTREACHED();
break;
}
}
return did_tab_backward;
}
#if defined(PDF_ENABLE_XFA)
void PDFiumEngine::UpdatePageCount() {
InvalidateAllPages();

@ -162,6 +162,11 @@ class PDFiumEngine : public PDFEngine,
FPDF_DOCUMENT doc() const;
FPDF_FORMHANDLE form() const;
// State transition when tabbing forward:
// None -> Document -> Page -> None (when focusable annotations on all pages
// are done).
enum class FocusElementType { kNone, kDocument, kPage };
private:
// This helper class is used to detect the difference in selection between
// construction and destruction. At destruction, it invalidates all the
@ -209,6 +214,7 @@ class PDFiumEngine : public PDFEngine,
};
friend class FormFillerTest;
friend class PDFiumEngineTabbingTest;
friend class PDFiumFormFiller;
friend class PDFiumTestBase;
friend class SelectionChangeInvalidator;
@ -580,6 +586,15 @@ class PDFiumEngine : public PDFEngine,
// Retrieves the version of the PDF (e.g. 1.4 or 2.0) as an enum.
PdfVersion GetDocumentVersion() const;
// This is a layer between OnKeyDown() and actual tab handling to facilitate
// testing.
bool HandleTabEvent(uint32_t modifiers);
// Helper functions to handle tab events.
bool HandleTabEventWithModifiers(uint32_t modifiers);
bool HandleTabForward(uint32_t modifiers);
bool HandleTabBackward(uint32_t modifiers);
PDFEngine::Client* const client_;
// The current document layout.
@ -681,6 +696,9 @@ class PDFiumEngine : public PDFEngine,
// Timer for touch long press detection.
base::OneShotTimer touch_timer_;
// The focus item type for the currently focused object.
FocusElementType focus_item_type_ = FocusElementType::kNone;
// Holds the zero-based page index of the last page that had the focused
// object.
int last_focused_page_ = -1;

@ -4,12 +4,14 @@
#include "pdf/pdfium/pdfium_engine.h"
#include "base/test/task_environment.h"
#include "pdf/document_layout.h"
#include "pdf/document_metadata.h"
#include "pdf/pdfium/pdfium_page.h"
#include "pdf/pdfium/pdfium_test_base.h"
#include "pdf/test/test_client.h"
#include "pdf/test/test_utils.h"
#include "ppapi/c/ppb_input_event.h"
#include "ppapi/cpp/size.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@ -222,4 +224,203 @@ TEST_F(PDFiumEngineTest, GetBadPdfVersion) {
}
} // namespace
class PDFiumEngineTabbingTest : public PDFiumTestBase {
public:
PDFiumEngineTabbingTest() = default;
~PDFiumEngineTabbingTest() override = default;
PDFiumEngineTabbingTest(const PDFiumEngineTabbingTest&) = delete;
PDFiumEngineTabbingTest& operator=(const PDFiumEngineTabbingTest&) = delete;
bool HandleTabEvent(PDFiumEngine* engine, uint32_t modifiers) {
return engine->HandleTabEvent(modifiers);
}
PDFiumEngine::FocusElementType GetFocusedElementType(PDFiumEngine* engine) {
return engine->focus_item_type_;
}
int GetLastFocusedPage(PDFiumEngine* engine) {
return engine->last_focused_page_;
}
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};
TEST_F(PDFiumEngineTabbingTest, TabbingForwardTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(1, GetLastFocusedPage(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, TabbingBackwardTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(1, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, TabbingWithModifiers) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing with ctrl modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_CONTROLKEY));
// Tabbing with alt modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_ALTKEY));
// Tab to bring document into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
// Tabbing with ctrl modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_CONTROLKEY));
// Tabbing with alt modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_ALTKEY));
// Tab to bring first page into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
// Tabbing with ctrl modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_CONTROLKEY));
// Tabbing with alt modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_ALTKEY));
}
TEST_F(PDFiumEngineTabbingTest, NoFocusableItemTabbingTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++ Page 2
*/
TestClient client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("hello_world2.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing forward.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
// Tabbing backward.
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
}
} // namespace chrome_pdf