mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
21b7eb1e18
Kindle 3 and 4 drive the catalog entirely through KOReader's FocusManager, but none of the plugin's own widgets answered a Focus event. The cursor moved invisibly, so Press looked like it activated something at random. Give every tappable widget a focus box. Draw it as an inner border: these pages lay themselves out from fixed dimensions, and a cursor that changed a widget's size would reflow the page under it. Menu marks its own rows with a hairline that upstream declines to thicken because that shifts the row's text; the same inner border gives those rows a visible cursor without that cost. Keep the cursor across the rebuilds these pages do constantly. The dashboard and detail pages replace their whole widget tree on a page turn, a reroll or a status change, and Menu's bookkeeping cannot place a cursor on rows of unequal width. Remember the footer by name rather than by row index: its controls are what cause the rebuild, so it has to survive even a move to another book. Record a page's cursor on the navigation stack as it is left, so returning lands where the reader was. Reach what was unreachable. Menu skips the title bar on Kindle keypads because its own title bar actions have hardware keys, but the search and refresh icons are this plugin's and have none. The footer paginator and return arrow join the layout too; on the detail page those chevrons step through books, which no key covers. Back walks up a level and closes only at the root, the rule Menu already applies to its own stack. The detail header walks vertically like the stack it draws, instead of eleven Right presses to reach Download, and opens on the button the page was opened for. Two of the fixes here are not specific to the D-Pad. The search and refresh icons were missing from every section page for all users: TitleBar clears its children and re-inits whenever a shrink-to-fit title changes, and a run-once guard stopped them being rebuilt. And a footer chevron hidden while focused kept an inverted frame, painting as a black block. Closes #880
517 lines
21 KiB
Lua
517 lines
21 KiB
Lua
-- Proves the D-Pad bookkeeping the custom catalog pages need on a device with
|
|
-- no touchscreen: that the title bar joins the focus layout on the Kindle
|
|
-- keypads Menu skips, and that the cursor survives a rebuild instead of being
|
|
-- teleported by Menu's uniform-grid focus math.
|
|
|
|
local device = {
|
|
hasDPad = function() return true end,
|
|
hasFewKeys = function() return false end,
|
|
}
|
|
package.loaded["device"] = device
|
|
package.loaded["ui/widget/focusmanager"] = { FOCUS_ONLY_ON_NT = 0 }
|
|
package.loaded["ui/event"] = {
|
|
new = function(_, name, ...) return { handler = "on" .. name, name, ... } end,
|
|
}
|
|
|
|
local focus_navigation = true
|
|
package.loaded["bookorbit_catalog_widgets"] = {
|
|
focusNavigation = function() return focus_navigation end,
|
|
}
|
|
|
|
package.path = "koreader-plugin/bookorbit.koplugin/?.lua;" .. package.path
|
|
|
|
local CatalogFocus = require("bookorbit_catalog_focus")
|
|
|
|
local function assertEqual(actual, expected, label)
|
|
if actual ~= expected then
|
|
error(string.format("%s: expected %s, got %s", label, tostring(expected), tostring(actual)))
|
|
end
|
|
end
|
|
|
|
local function titleBar(buttons)
|
|
return {
|
|
generateHorizontalLayout = function()
|
|
return #buttons > 0 and { buttons } or {}
|
|
end,
|
|
generateVerticalLayout = function()
|
|
local rows = {}
|
|
for _, button in ipairs(buttons) do
|
|
table.insert(rows, { button })
|
|
end
|
|
return rows
|
|
end,
|
|
}
|
|
end
|
|
|
|
local function newCatalog(opts)
|
|
opts = opts or {}
|
|
local catalog = {
|
|
layout = opts.layout or {},
|
|
selected = { x = opts.x or 1, y = opts.y or 1 },
|
|
current_context = opts.context,
|
|
title_bar = opts.title_bar,
|
|
title_bar_layout_rows = opts.title_bar_layout_rows,
|
|
custom_focus_key = opts.custom_focus_key,
|
|
custom_focus = opts.custom_focus,
|
|
}
|
|
function catalog:moveFocusTo(x, y, flags)
|
|
self.moved = { x = x, y = y, flags = flags }
|
|
self.selected.x, self.selected.y = x, y
|
|
end
|
|
CatalogFocus.install(catalog)
|
|
return catalog
|
|
end
|
|
|
|
-- The title bar goes in as one row on a full keypad, and the cursor's row
|
|
-- index shifts to stay on the widget it was already on.
|
|
local catalog = newCatalog{
|
|
layout = { { "hero" }, { "a", "b" } },
|
|
title_bar = titleBar{ "back", "search", "refresh", "menu" },
|
|
y = 1,
|
|
}
|
|
catalog:mergeTitleBarIntoLayout()
|
|
assertEqual(#catalog.layout, 3, "the title bar adds a single row on a full keypad")
|
|
assertEqual(#catalog.layout[1], 4, "every title bar button shares that row")
|
|
assertEqual(catalog.layout[1][2], "search", "the plugin's own search icon is reachable")
|
|
assertEqual(catalog.title_bar_layout_rows, 1, "the merged row count is recorded")
|
|
assertEqual(catalog.selected.y, 2, "the cursor follows its widget down the layout")
|
|
|
|
-- A few-keys device spends "Right" on the context menu, so its title bar has to
|
|
-- stack instead.
|
|
device.hasFewKeys = function() return true end
|
|
catalog = newCatalog{
|
|
layout = { { "hero" } },
|
|
title_bar = titleBar{ "back", "search" },
|
|
y = 1,
|
|
}
|
|
catalog:mergeTitleBarIntoLayout()
|
|
assertEqual(#catalog.layout, 3, "a few-keys title bar stacks one button per row")
|
|
assertEqual(catalog.title_bar_layout_rows, 2, "both stacked rows are recorded")
|
|
assertEqual(catalog.selected.y, 3, "the cursor shifts by both rows")
|
|
device.hasFewKeys = function() return false end
|
|
|
|
-- Without a D-Pad there is no cursor to place, and a page with no title bar
|
|
-- must not claim rows it did not add.
|
|
catalog = newCatalog{ layout = { { "a" } }, title_bar = titleBar{ "back" } }
|
|
device.hasDPad = function() return false end
|
|
catalog:mergeTitleBarIntoLayout()
|
|
assertEqual(#catalog.layout, 1, "a device with no D-Pad skips the merge")
|
|
assertEqual(catalog.title_bar_layout_rows, 0, "and records no merged rows")
|
|
device.hasDPad = function() return true end
|
|
|
|
catalog = newCatalog{ layout = { { "a" } } }
|
|
catalog:mergeTitleBarIntoLayout()
|
|
assertEqual(catalog.title_bar_layout_rows, 0, "a page with no title bar merges nothing")
|
|
|
|
-- The two devices this exists for. Kindle 3 and Kindle 4 are the models the bug
|
|
-- was reported on, and they are exactly the ones upstream Menu refuses to merge
|
|
-- a title bar for (K3 has a Sym key, K4 an on-screen keyboard). Neither sets
|
|
-- hasFewKeys, so both take the single-row title bar.
|
|
for _, profile in ipairs({
|
|
{ model = "Kindle3", has_sym_key = true, has_screen_kb = false },
|
|
{ model = "Kindle4", has_sym_key = false, has_screen_kb = true },
|
|
}) do
|
|
device.hasSymKey = function() return profile.has_sym_key end
|
|
device.hasScreenKB = function() return profile.has_screen_kb end
|
|
catalog = newCatalog{
|
|
layout = { { "hero" } },
|
|
title_bar = titleBar{ "back", "search", "refresh" },
|
|
y = 1,
|
|
}
|
|
catalog:mergeTitleBarIntoLayout()
|
|
assertEqual(catalog.title_bar_layout_rows, 1,
|
|
profile.model .. " gets its title bar as one row")
|
|
assertEqual(#catalog.layout[1], 3,
|
|
profile.model .. " can reach every title bar button")
|
|
assertEqual(catalog.selected.y, 2,
|
|
profile.model .. " shifts the cursor past the merged row")
|
|
end
|
|
device.hasSymKey = nil
|
|
device.hasScreenKB = nil
|
|
|
|
-- Opening a page for the first time records its key but has nothing to restore.
|
|
catalog = newCatalog{ context = { kind = "dashboard" }, x = 3, y = 4 }
|
|
catalog:saveCustomFocus()
|
|
assertEqual(catalog.custom_focus, nil, "a page opened fresh has no remembered cursor")
|
|
assertEqual(catalog.custom_focus_key, "dashboard", "but its key is recorded for the next rebuild")
|
|
|
|
-- Refreshing the same page keeps the cursor.
|
|
catalog:saveCustomFocus()
|
|
assertEqual(catalog.custom_focus.x, 3, "a rebuild of the same page remembers the column")
|
|
assertEqual(catalog.custom_focus.y, 4, "and the row")
|
|
|
|
-- Opening a different page does not.
|
|
catalog.current_context = { kind = "detail", detail = { id = 7 } }
|
|
catalog:saveCustomFocus()
|
|
assertEqual(catalog.custom_focus, nil, "opening another page starts its cursor from scratch")
|
|
assertEqual(catalog.custom_focus_key, "detail:7", "and the detail key names the book")
|
|
|
|
catalog.current_context = { kind = "detail", detail = { id = 8 } }
|
|
catalog:saveCustomFocus()
|
|
assertEqual(catalog.custom_focus, nil, "another book is another page")
|
|
|
|
-- A rebuilt layout can be shorter than the one the cursor was remembered
|
|
-- against: a shelf that lost a row, or a header that lost its paging chevrons.
|
|
catalog = newCatalog{
|
|
layout = { { "a", "b" }, { "c" }, { "d", "e", "f", "g" } },
|
|
custom_focus_key = "dashboard",
|
|
custom_focus = { x = 5, y = 9 },
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 3, "a row past the end clamps to the last row")
|
|
assertEqual(catalog.moved.x, 4, "and a column past the end clamps to that row's width")
|
|
assertEqual(catalog.custom_focus, nil, "the remembered cursor is consumed by the restore")
|
|
|
|
catalog = newCatalog{
|
|
layout = { { "a", "b", "c" }, { "d" } },
|
|
custom_focus_key = "dashboard",
|
|
custom_focus = { x = 2, y = 1 },
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.x, 2, "a cursor still inside the layout is left alone")
|
|
assertEqual(catalog.moved.y, 1, "on both axes")
|
|
|
|
-- An empty row is skipped rather than focused.
|
|
catalog = newCatalog{
|
|
layout = { { "a" }, { "b" }, {} },
|
|
custom_focus_key = "dashboard",
|
|
custom_focus = { x = 1, y = 3 },
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 2, "an empty row hands the cursor to the one above it")
|
|
|
|
-- A page opened fresh starts below the title bar, on content.
|
|
catalog = newCatalog{
|
|
layout = { { "back", "search" }, { "hero" }, { "a", "b" } },
|
|
custom_focus_key = "dashboard",
|
|
title_bar_layout_rows = 1,
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 2, "a fresh page opens on its first content row")
|
|
assertEqual(catalog.moved.x, 1, "at the start of that row")
|
|
|
|
-- Section headers carry paging chevrons that are disabled on page one, so a
|
|
-- fresh cursor skips past them to the books rather than opening on a dead
|
|
-- control.
|
|
local chrome = { "prev", "next" }
|
|
chrome.is_chrome = true
|
|
catalog = newCatalog{
|
|
layout = { { "back" }, chrome, { "book" } },
|
|
custom_focus_key = "dashboard",
|
|
title_bar_layout_rows = 1,
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 3, "a fresh page skips a section header's controls")
|
|
|
|
-- Chrome is only skipped when opening; a remembered cursor may well be sitting
|
|
-- on a chevron the reader was paging with.
|
|
catalog = newCatalog{
|
|
layout = { { "back" }, chrome, { "book" } },
|
|
custom_focus_key = "dashboard",
|
|
custom_focus = { x = 2, y = 2 },
|
|
title_bar_layout_rows = 1,
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 2, "a remembered cursor stays on its chevron")
|
|
assertEqual(catalog.moved.x, 2, "in the column it was paging from")
|
|
|
|
-- A page that is nothing but chrome still has to put the cursor somewhere.
|
|
local only_chrome = { "prev", "next" }
|
|
only_chrome.is_chrome = true
|
|
catalog = newCatalog{
|
|
layout = { { "back" }, only_chrome },
|
|
custom_focus_key = "dashboard",
|
|
title_bar_layout_rows = 1,
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 2, "a page with no content row falls back to its first row")
|
|
|
|
-- The mosaic and list views are the uniform grids Menu already handles.
|
|
catalog = newCatalog{ layout = { { "a" } } }
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved, nil, "a plain book grid is left to Menu's own bookkeeping")
|
|
|
|
-- A touchscreen has no cursor to keep.
|
|
focus_navigation = false
|
|
catalog = newCatalog{ context = { kind = "dashboard" }, layout = { { "a" } }, x = 2, y = 2 }
|
|
catalog:saveCustomFocus()
|
|
assertEqual(catalog.custom_focus_key, nil, "a touch device tracks no page key")
|
|
assertEqual(catalog.custom_focus, nil, "and remembers no cursor")
|
|
catalog.custom_focus_key = "dashboard"
|
|
catalog.custom_focus = { x = 2, y = 2 }
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved, nil, "and never moves the cursor")
|
|
focus_navigation = true
|
|
|
|
-- The footer's return arrow is a Button Menu never puts in the focus layout, so
|
|
-- Back is the only way up a level on a device that cannot tap it.
|
|
catalog = newCatalog{}
|
|
catalog.stack = { "dashboard", "books" }
|
|
assertEqual(catalog:shouldReturnOnBack(), true, "Back walks up while there is a level to return to")
|
|
catalog.stack = {}
|
|
assertEqual(catalog:shouldReturnOnBack(), false, "Back closes the catalog at the root")
|
|
catalog.stack = nil
|
|
assertEqual(catalog:shouldReturnOnBack(), false, "a catalog with no stack yet closes on Back")
|
|
|
|
-- A touch device can tap the arrow, so Back keeps closing outright: a multiswipe
|
|
-- to leave should not turn into one swipe per level browsed.
|
|
focus_navigation = false
|
|
catalog = newCatalog{}
|
|
catalog.stack = { "dashboard", "books" }
|
|
assertEqual(catalog:shouldReturnOnBack(), false, "a touch device still closes on Back")
|
|
focus_navigation = true
|
|
|
|
|
|
-- A page may nominate the row it exists for. The detail page marks its Read or
|
|
-- Download button, so the cursor opens on the thing the reader came to press
|
|
-- rather than five rows above it.
|
|
local primary = { "download" }
|
|
primary.is_primary = true
|
|
local header_chrome = { "prev", "next" }
|
|
header_chrome.is_chrome = true
|
|
catalog = newCatalog{
|
|
layout = { { "back" }, header_chrome, { "meta" }, primary },
|
|
custom_focus_key = "detail:7",
|
|
title_bar_layout_rows = 1,
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 4, "a fresh page opens on the row it nominated")
|
|
|
|
-- A remembered cursor still wins: the reader was somewhere for a reason.
|
|
catalog = newCatalog{
|
|
layout = { { "back" }, { "meta" }, primary },
|
|
custom_focus_key = "detail:7",
|
|
custom_focus = { x = 1, y = 2 },
|
|
title_bar_layout_rows = 1,
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 2, "a rebuild keeps the cursor where it was")
|
|
|
|
-- The footer's paginator and return arrow join the layout as a last row, so a
|
|
-- reader who never learns the page-turn keys can still reach them. Only what is
|
|
-- actually on screen goes in: the plugin hides the chevrons on pages that do
|
|
-- not page.
|
|
local function footerCatalog(opts)
|
|
local c = newCatalog{}
|
|
for k, v in pairs(opts) do
|
|
-- Model a real widget: the footer tells hidden controls they lost focus.
|
|
if type(v) == "table" and not v.handleEvent then
|
|
v.handleEvent = function(self, event)
|
|
if event.handler == "onUnfocus" then self.focused = false end
|
|
return true
|
|
end
|
|
end
|
|
c[k] = v
|
|
end
|
|
return c
|
|
end
|
|
|
|
local chevrons = footerCatalog{
|
|
page_return_arrow = { name = "return" },
|
|
page_info_first_chev = { name = "first" },
|
|
page_info_left_chev = { name = "prev" },
|
|
page_info_text = { name = "text", text = "Page 1 of 15" },
|
|
page_info_right_chev = { name = "next" },
|
|
page_info_last_chev = { name = "last" },
|
|
}
|
|
local footer = chevrons:footerFocusRow()
|
|
assertEqual(#footer, 6, "every visible footer control is reachable")
|
|
assertEqual(footer[1].name, "return", "the return arrow comes first, as drawn")
|
|
assertEqual(footer[4].name, "text", "the page indicator sits between the chevrons")
|
|
assertEqual(footer.is_chrome, true, "the footer is chrome, so a fresh page skips it")
|
|
|
|
-- A page that does not page hides its chevrons and empties the indicator.
|
|
local dashboard_footer = footerCatalog{
|
|
page_return_arrow = { name = "return", hidden = true },
|
|
page_info_first_chev = { name = "first", hidden = true },
|
|
page_info_left_chev = { name = "prev", hidden = true },
|
|
page_info_text = { name = "text", text = "" },
|
|
page_info_right_chev = { name = "next", hidden = true },
|
|
page_info_last_chev = { name = "last", hidden = true },
|
|
}
|
|
assertEqual(dashboard_footer:footerFocusRow(), nil, "a page with no footer controls adds no row")
|
|
|
|
-- A return arrow with nothing beside it is still worth reaching.
|
|
local only_return = footerCatalog{
|
|
page_return_arrow = { name = "return" },
|
|
page_info_text = { name = "text", text = "" },
|
|
}
|
|
local lone = only_return:footerFocusRow()
|
|
assertEqual(#lone, 1, "a lone return arrow still gets a row")
|
|
assertEqual(lone[1].name, "return", "holding the arrow")
|
|
|
|
-- A touch device taps the footer directly.
|
|
focus_navigation = false
|
|
assertEqual(chevrons:footerFocusRow(), nil, "a touch device adds no footer row")
|
|
focus_navigation = true
|
|
|
|
-- The footer is the one row that has to survive a change of page: its controls
|
|
-- are what cause the rebuild, so pressing "next page" or "next book" from there
|
|
-- must leave the cursor on the button just pressed.
|
|
local function footerRow(...)
|
|
local row = { ... }
|
|
row.id = "footer"
|
|
row.is_chrome = true
|
|
return row
|
|
end
|
|
|
|
-- The row index shifts with however much content the page has; the id does not.
|
|
catalog = newCatalog{
|
|
layout = { { "a" }, { "b" }, { "c" }, footerRow("return", "prev", "next") },
|
|
custom_focus_key = "books",
|
|
custom_focus = { x = 3, y = 4, row_id = "footer" },
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 4, "the footer is found at its new row index")
|
|
assertEqual(catalog.moved.x, 3, "on the same control")
|
|
|
|
catalog = newCatalog{
|
|
layout = { { "a" }, footerRow("return", "prev", "next") },
|
|
custom_focus_key = "books",
|
|
custom_focus = { x = 3, y = 9, row_id = "footer" },
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.y, 2, "a shorter page still finds the footer")
|
|
assertEqual(catalog.moved.x, 3, "keeping the control that caused the rebuild")
|
|
|
|
-- Even where the page has no key of its own: the book grid is Menu's to manage,
|
|
-- but its footer is ours.
|
|
catalog = newCatalog{
|
|
layout = { { "a" }, footerRow("return", "next") },
|
|
custom_focus = { x = 2, y = 2, row_id = "footer" },
|
|
}
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved ~= nil, true, "a grid with no page key still restores its footer")
|
|
assertEqual(catalog.moved.x, 2, "on the pressed control")
|
|
|
|
-- Resting on the footer is remembered whatever page comes next.
|
|
catalog = newCatalog{
|
|
layout = { { "a" }, footerRow("return", "next") },
|
|
context = { kind = "detail", detail = { id = 1 } },
|
|
custom_focus_key = "detail:99",
|
|
x = 2, y = 2,
|
|
}
|
|
catalog:saveCustomFocus()
|
|
assertEqual(catalog.custom_focus ~= nil, true, "the footer cursor survives moving to another book")
|
|
assertEqual(catalog.custom_focus.row_id, "footer", "and remembers it was the footer")
|
|
|
|
-- Off the footer, a change of page still starts the new one fresh.
|
|
catalog = newCatalog{
|
|
layout = { { "a" }, { "b" } },
|
|
context = { kind = "detail", detail = { id = 1 } },
|
|
custom_focus_key = "detail:99",
|
|
x = 1, y = 2,
|
|
}
|
|
catalog:saveCustomFocus()
|
|
assertEqual(catalog.custom_focus, nil, "an ordinary row does not follow the reader to another page")
|
|
|
|
-- Returning to a page restores the cursor it was left at, across any number of
|
|
-- pages visited in between and whether or not the page keys its own focus.
|
|
catalog = newCatalog{ layout = { { "a", "b" }, { "c" } }, x = 2, y = 1 }
|
|
local left_at = catalog:captureFocus()
|
|
assertEqual(left_at.x, 2, "leaving a page captures the column")
|
|
assertEqual(left_at.y, 1, "and the row")
|
|
|
|
catalog = newCatalog{ layout = { { "a", "b" }, { "c" } } }
|
|
catalog:restoreFocusOnNextUpdate(left_at)
|
|
catalog:saveCustomFocus()
|
|
catalog:restoreCustomFocus()
|
|
assertEqual(catalog.moved.x, 2, "returning puts the cursor back in its column")
|
|
assertEqual(catalog.moved.y, 1, "and its row")
|
|
|
|
-- A touch device captures nothing to restore.
|
|
focus_navigation = false
|
|
catalog = newCatalog{ layout = { { "a" } }, x = 1, y = 1 }
|
|
assertEqual(catalog:captureFocus(), nil, "a touch device captures no cursor")
|
|
catalog:restoreFocusOnNextUpdate({ x = 1, y = 1 })
|
|
assertEqual(catalog.pending_focus, nil, "and accepts none")
|
|
focus_navigation = true
|
|
|
|
-- A focus layout may only name buttons the title bar actually draws. TitleBar
|
|
-- clears its children and re-inits whenever a shrink-to-fit title changes, which
|
|
-- drops any button appended afterwards while leaving the field pointing at it.
|
|
-- Trusting the field sent the cursor to controls that were not on screen - and
|
|
-- the same drop hid those buttons from touch users too.
|
|
local ORDER = { "left_button", "search_button", "refresh_button", "right_button" }
|
|
local APPENDED = { search_button = true, refresh_button = true }
|
|
local function titleBar(fields, drawn)
|
|
local b = {}
|
|
for k, v in pairs(fields) do b[k] = v end
|
|
for _, child in ipairs(drawn or {}) do table.insert(b, child) end
|
|
return b
|
|
end
|
|
|
|
local left, search, refresh = { "left" }, { "search" }, { "refresh" }
|
|
|
|
-- All three drawn: all three reachable.
|
|
local drawnButtons = CatalogFocus.drawnTitleBarButtons(
|
|
titleBar({ left_button = left, search_button = search, refresh_button = refresh },
|
|
{ search, refresh }), ORDER, APPENDED)
|
|
assertEqual(#drawnButtons, 3, "every drawn button is reachable")
|
|
assertEqual(drawnButtons[2], search, "in left-to-right order")
|
|
|
|
-- Re-init dropped them from the group but left the fields set.
|
|
drawnButtons = CatalogFocus.drawnTitleBarButtons(
|
|
titleBar({ left_button = left, search_button = search, refresh_button = refresh }, {}),
|
|
ORDER, APPENDED)
|
|
assertEqual(#drawnButtons, 1, "buttons dropped from the group are not reachable")
|
|
assertEqual(drawnButtons[1], left, "leaving only the bar's own button")
|
|
|
|
-- One of the two survived.
|
|
drawnButtons = CatalogFocus.drawnTitleBarButtons(
|
|
titleBar({ left_button = left, search_button = search, refresh_button = refresh },
|
|
{ refresh }), ORDER, APPENDED)
|
|
assertEqual(#drawnButtons, 2, "a partially rebuilt bar reports only what it drew")
|
|
assertEqual(drawnButtons[2], refresh, "and keeps their order")
|
|
|
|
-- TitleBar's own buttons are nested rather than direct children, so they are
|
|
-- taken on trust and must not be filtered out.
|
|
drawnButtons = CatalogFocus.drawnTitleBarButtons(
|
|
titleBar({ left_button = left, right_button = { "right" } }, {}), ORDER, APPENDED)
|
|
assertEqual(#drawnButtons, 2, "the bar's own buttons are trusted without membership")
|
|
|
|
assertEqual(#CatalogFocus.drawnTitleBarButtons(titleBar({}, {}), ORDER, APPENDED), 0,
|
|
"a bar with no buttons reports none")
|
|
|
|
-- A footer control that has just been hidden is no longer in the layout, so no
|
|
-- Unfocus would ever reach it - and a hidden Button still paints its frame, so a
|
|
-- stale inverted one shows up as a black block where the chevron used to be.
|
|
local function footerWidget(name, hidden)
|
|
return {
|
|
name = name, hidden = hidden, focused = not not hidden,
|
|
handleEvent = function(self, event)
|
|
if event.handler == "onUnfocus" or event[1] == "Unfocus" then
|
|
self.focused = false
|
|
end
|
|
return true
|
|
end,
|
|
}
|
|
end
|
|
|
|
local shown_chev = footerWidget("next", false)
|
|
local hidden_chev = footerWidget("last", true)
|
|
local page_text = footerWidget("text", false)
|
|
page_text.text = "Page 1 of 3"
|
|
catalog = newCatalog{}
|
|
catalog.page_return_arrow = footerWidget("return", false)
|
|
catalog.page_info_first_chev = footerWidget("first", true)
|
|
catalog.page_info_left_chev = footerWidget("prev", true)
|
|
catalog.page_info_text = page_text
|
|
catalog.page_info_right_chev = shown_chev
|
|
catalog.page_info_last_chev = hidden_chev
|
|
|
|
local built = catalog:footerFocusRow()
|
|
assertEqual(#built, 3, "only the controls actually on screen are reachable")
|
|
assertEqual(hidden_chev.focused, false, "a hidden chevron is told it lost focus")
|
|
assertEqual(catalog.page_info_first_chev.focused, false, "every hidden control is")
|
|
assertEqual(shown_chev.focused, false, "a shown one is left alone to be focused normally")
|
|
|
|
-- An emptied page indicator is excluded too: it is blanked rather than hidden.
|
|
page_text.text = ""
|
|
built = catalog:footerFocusRow()
|
|
for _, w in ipairs(built) do
|
|
assertEqual(w ~= page_text, true, "an empty page indicator is not reachable")
|
|
end
|
|
|
|
print("bookorbit_focus_navigation_test.lua: ok")
|