2017-08-29 19:11:16 +00:00
|
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
|
|
local Button = require("ui/widget/button")
|
|
|
|
local Device = require("device")
|
|
|
|
local FocusManager = require("ui/widget/focusmanager")
|
2013-10-18 20:38:07 +00:00
|
|
|
local HorizontalGroup = require("ui/widget/horizontalgroup")
|
2017-08-29 19:11:16 +00:00
|
|
|
local LineWidget = require("ui/widget/linewidget")
|
2017-09-13 14:56:20 +00:00
|
|
|
local Size = require("ui/size")
|
2015-09-13 08:09:00 +00:00
|
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
2013-10-18 20:38:07 +00:00
|
|
|
local VerticalSpan = require("ui/widget/verticalspan")
|
|
|
|
local Geom = require("ui/geometry")
|
2015-09-13 08:09:00 +00:00
|
|
|
local Screen = Device.screen
|
2013-06-15 15:13:19 +00:00
|
|
|
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
|
|
|
local ButtonTable = FocusManager:extend{
|
2021-09-25 08:51:58 +00:00
|
|
|
width = nil,
|
2023-03-23 17:37:40 +00:00
|
|
|
-- If requested, allow ButtonTable to shrink itself if 'width' can
|
|
|
|
-- be reduced without any truncation or font size getting smaller.
|
|
|
|
shrink_unneeded_width = false,
|
|
|
|
-- But we won't go below this: buttons are tapable, we want some
|
|
|
|
-- minimal width so they are easy to tap (this is mostly needed
|
|
|
|
-- for CJK languages where button text can be one or two glyphs).
|
|
|
|
shrink_min_width = Screen:scaleBySize(100),
|
|
|
|
|
2014-03-13 13:52:43 +00:00
|
|
|
buttons = {
|
|
|
|
{
|
|
|
|
{text="OK", enabled=true, callback=nil},
|
|
|
|
{text="Cancel", enabled=false, callback=nil},
|
|
|
|
},
|
|
|
|
},
|
2017-09-13 14:56:20 +00:00
|
|
|
sep_width = Size.line.medium,
|
2014-03-13 13:52:43 +00:00
|
|
|
zero_sep = false,
|
2013-06-15 15:13:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function ButtonTable:init()
|
2021-09-25 08:51:58 +00:00
|
|
|
self.width = self.width or math.floor(math.min(Screen:getWidth(), Screen:getHeight()) * 0.9)
|
2016-05-26 06:09:49 +00:00
|
|
|
self.buttons_layout = {}
|
2018-08-06 19:16:30 +00:00
|
|
|
self.button_by_id = {}
|
2015-09-13 08:09:00 +00:00
|
|
|
self.container = VerticalGroup:new{ width = self.width }
|
2023-03-23 17:37:40 +00:00
|
|
|
self[1] = self.container
|
2014-03-13 13:52:43 +00:00
|
|
|
if self.zero_sep then
|
2017-10-08 15:53:25 +00:00
|
|
|
-- If we're asked to add a first line, don't add a vspan before: caller
|
|
|
|
-- must do its own padding before.
|
2023-05-11 18:23:45 +00:00
|
|
|
self:addVerticalSeparator()
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2016-05-26 06:09:49 +00:00
|
|
|
local row_cnt = #self.buttons
|
2023-03-23 17:37:40 +00:00
|
|
|
local table_min_needed_width = -1
|
2016-05-26 06:09:49 +00:00
|
|
|
for i = 1, row_cnt do
|
2023-05-11 18:23:45 +00:00
|
|
|
self:addVerticalSpan()
|
2018-03-18 10:42:35 +00:00
|
|
|
local buttons_layout_line = {}
|
2014-03-13 13:52:43 +00:00
|
|
|
local horizontal_group = HorizontalGroup:new{}
|
2016-05-26 06:09:49 +00:00
|
|
|
local row = self.buttons[i]
|
|
|
|
local column_cnt = #row
|
2023-03-23 17:37:36 +00:00
|
|
|
local available_width = self.width - self.sep_width * (column_cnt - 1)
|
|
|
|
local unspecified_width_buttons = 0
|
|
|
|
for j = 1, column_cnt do
|
|
|
|
local btn_entry = row[j]
|
|
|
|
if btn_entry.width then
|
|
|
|
available_width = available_width - btn_entry.width
|
|
|
|
else
|
|
|
|
unspecified_width_buttons = unspecified_width_buttons + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
local default_button_width = math.floor(available_width / unspecified_width_buttons)
|
2023-03-23 17:37:40 +00:00
|
|
|
local min_needed_button_width = -1
|
2016-05-26 06:09:49 +00:00
|
|
|
for j = 1, column_cnt do
|
|
|
|
local btn_entry = row[j]
|
2014-03-13 13:52:43 +00:00
|
|
|
local button = Button:new{
|
2016-05-26 06:09:49 +00:00
|
|
|
text = btn_entry.text,
|
2019-06-28 02:46:16 +00:00
|
|
|
text_func = btn_entry.text_func,
|
2023-12-14 05:50:54 +00:00
|
|
|
checked_func = btn_entry.checked_func,
|
2023-05-11 18:23:45 +00:00
|
|
|
lang = btn_entry.lang,
|
2021-09-09 23:07:31 +00:00
|
|
|
icon = btn_entry.icon,
|
2021-09-11 06:27:38 +00:00
|
|
|
icon_width = btn_entry.icon_width,
|
|
|
|
icon_height = btn_entry.icon_height,
|
2022-09-17 22:11:03 +00:00
|
|
|
align = btn_entry.align,
|
2016-05-26 06:09:49 +00:00
|
|
|
enabled = btn_entry.enabled,
|
2023-03-23 17:37:34 +00:00
|
|
|
enabled_func = btn_entry.enabled_func,
|
2023-01-01 15:11:10 +00:00
|
|
|
callback = function()
|
|
|
|
if self.show_parent and self.show_parent.movable then
|
|
|
|
self.show_parent.movable:resetEventState()
|
|
|
|
end
|
|
|
|
btn_entry.callback()
|
|
|
|
end,
|
2019-02-15 23:42:27 +00:00
|
|
|
hold_callback = btn_entry.hold_callback,
|
2022-09-22 06:46:15 +00:00
|
|
|
allow_hold_when_disabled = btn_entry.allow_hold_when_disabled,
|
Assorted fixes after #7118 (#7161)
* I'd failed to notice that ButtonTable *also* instantiates seven billion Buttons on each update. Unfortunately, that one is way trickier to fix properly, so, work around its behavior in Button. (This fixes multiple issues with stuff using ButtonTable, which is basically anything with a persistent set of buttons. A good and easy test-case is the dictionary popup, e.g., the Highlight button changes text, and the next/prev dic buttons change state. All that, and more, was broken ;p).
* Handle corner-cases related to VirtualKeyboard (e.g., Terminal & Text Editor), which screwed with both TouchMenu & Button heuristics because it's weird.
* Flag a the dictionary switch buttons as vsync
(They trigger a partial repaint of the dictionary content).
* Flag the ReaderSearch buttons as vsync
They very obviously trigger a partial repaint, much like SkimTo ;p.
2021-01-18 15:51:25 +00:00
|
|
|
vsync = btn_entry.vsync,
|
2023-03-23 17:37:36 +00:00
|
|
|
width = btn_entry.width or default_button_width,
|
2014-03-13 13:52:43 +00:00
|
|
|
bordersize = 0,
|
|
|
|
margin = 0,
|
2019-11-01 00:52:05 +00:00
|
|
|
padding = Size.padding.buttontable, -- a bit taller than standalone buttons, for easier tap
|
2023-05-11 18:23:45 +00:00
|
|
|
padding_h = btn_entry.align == "left" and Size.padding.large or Size.padding.button,
|
2023-03-23 17:37:29 +00:00
|
|
|
-- allow text to take more of the horizontal space if centered
|
2023-03-31 16:35:27 +00:00
|
|
|
avoid_text_truncation = btn_entry.avoid_text_truncation,
|
2022-09-17 22:11:03 +00:00
|
|
|
text_font_face = btn_entry.font_face,
|
|
|
|
text_font_size = btn_entry.font_size,
|
|
|
|
text_font_bold = btn_entry.font_bold,
|
2014-05-01 10:37:12 +00:00
|
|
|
show_parent = self.show_parent,
|
2014-03-13 13:52:43 +00:00
|
|
|
}
|
2023-03-23 17:37:40 +00:00
|
|
|
if self.shrink_unneeded_width and not btn_entry.width and min_needed_button_width ~= false then
|
|
|
|
-- We gather the largest min width of all buttons without a specified width,
|
|
|
|
-- and will see how it does when this largest min width is applied to all
|
|
|
|
-- buttons (without a specified width): we still want to keep them the same
|
|
|
|
-- size and balanced.
|
|
|
|
local min_width = button:getMinNeededWidth()
|
|
|
|
if min_width then
|
|
|
|
if min_needed_button_width < min_width then
|
|
|
|
min_needed_button_width = min_width
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- If any one button in this row can't be made smaller, give up
|
|
|
|
min_needed_button_width = false
|
|
|
|
end
|
|
|
|
end
|
2018-08-06 19:16:30 +00:00
|
|
|
if btn_entry.id then
|
|
|
|
self.button_by_id[btn_entry.id] = button
|
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
local button_dim = button:getSize()
|
|
|
|
local vertical_sep = LineWidget:new{
|
2019-11-11 09:05:28 +00:00
|
|
|
background = Blitbuffer.COLOR_GRAY,
|
2014-03-13 13:52:43 +00:00
|
|
|
dimen = Geom:new{
|
|
|
|
w = self.sep_width,
|
|
|
|
h = button_dim.h,
|
|
|
|
}
|
|
|
|
}
|
2018-03-18 10:42:35 +00:00
|
|
|
buttons_layout_line[j] = button
|
2014-03-13 13:52:43 +00:00
|
|
|
table.insert(horizontal_group, button)
|
2016-05-26 06:09:49 +00:00
|
|
|
if j < column_cnt then
|
2014-03-13 13:52:43 +00:00
|
|
|
table.insert(horizontal_group, vertical_sep)
|
|
|
|
end
|
|
|
|
end -- end for each button
|
2015-09-13 08:09:00 +00:00
|
|
|
table.insert(self.container, horizontal_group)
|
2023-05-11 18:23:45 +00:00
|
|
|
self:addVerticalSpan()
|
2016-05-26 06:09:49 +00:00
|
|
|
if i < row_cnt then
|
2023-05-11 18:23:45 +00:00
|
|
|
self:addVerticalSeparator()
|
2014-03-13 13:52:43 +00:00
|
|
|
end
|
2018-03-18 10:42:35 +00:00
|
|
|
if column_cnt > 0 then
|
Tame some ButtonTable users into re-using Buttontable instances if possible (#7166)
* QuickDictLookup, ImageViewer, NumberPicker: Smarter `update` that will re-use most of the widget's layout instead of re-instantiating all the things.
* SpinWidget/DoubleSpinWidget: The NumberPicker change above renders a hack to preserve alpha on these widgets almost unnecessary. Also fixed said hack to also apply to the center, value button.
* Button: Don't re-instantiate the frame in setText/setIcon when unnecessary (e.g., no change at all, or no layout change).
* Button: Add a refresh method that repaints and refreshes a *specific* Button (provided it's been painted once) all on its lonesome.
* ConfigDialog: Free everything that's going to be re-instatiated on update
* A few more post #7118 fixes:
* SkimTo: Always flag the chapter nav buttons as vsync
* Button: Fix the highlight on rounded buttons when vsync is enabled (e.g., it's now entirely visible, instead of showing a weird inverted corner glitch).
* Some more heuristic tweaks in Menu/TouchMenu/Button/IconButton
* ButtonTable: fix the annoying rounding issue I'd noticed in #7054 ;).
* Enable dithering in TextBoxWidget (e.g., in the Wikipedia full view). This involved moving the HW dithering align fixup to base, where it always ought to have been ;).
* Switch a few widgets that were using "partial" on close to "ui", or, more rarely, "flashui". The intent being to limit "partial" purely to the Reader, because it has a latency cost when mixed with other refreshes, which happens often enough in UI ;).
* Minor documentation tweaks around UIManager's `setDirty` to reflect that change.
* ReaderFooter: Force a footer repaint on resume if it is visible (otherwise, just update it).
* ReaderBookmark: In the same vein, don't repaint an invisible footer on bookmark count changes.
2021-01-28 23:20:15 +00:00
|
|
|
-- Only add lines that are not separator to the focusmanager
|
2018-03-18 10:42:35 +00:00
|
|
|
table.insert(self.buttons_layout, buttons_layout_line)
|
|
|
|
end
|
2023-03-23 17:37:40 +00:00
|
|
|
if self.shrink_unneeded_width and table_min_needed_width ~= false then
|
|
|
|
if min_needed_button_width then
|
|
|
|
if min_needed_button_width >= 0 and min_needed_button_width < default_button_width then
|
|
|
|
local row_min_width = self.width - (default_button_width - min_needed_button_width)*unspecified_width_buttons
|
|
|
|
if table_min_needed_width < row_min_width then
|
|
|
|
table_min_needed_width = row_min_width
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- If any one row can't be made smaller, give up
|
|
|
|
table_min_needed_width = false
|
|
|
|
end
|
|
|
|
end
|
2014-03-13 13:52:43 +00:00
|
|
|
end -- end for each button line
|
A few graphics fixes after #4541 (#4554)
* Various FocusManager related tweaks to limit its usage to devices with a DPad, and prevent initial button highlights in Dialogs on devices where it makes no sense (i.e., those without a DPad. And even on DPad devices, I'm not even sure how we'd go about making one of those pop up anyway, because no Touch ;)!).
* One mysterious fix to text-only Buttons so that the flash_ui highlight always works, and always honors `FrameContainer`'s pill shape. (Before that, an unhighlight on a text button with a callback that didn't repaint anything [say, the find first/find last buttons in the Reader's search bar when you're already on the first/last match] would do a square black highlight, and a white pill-shaped unhighlight (leaving the black corners visible)).
The workaround makes *absolutely* no sense to me (as `self[1] -> self.frame`, AFAICT), but it works, and ensures all highlights/unhighlights are pill-shaped, so at least we're not doing maths for rounded corners for nothing ;).
2019-02-07 23:56:32 +00:00
|
|
|
if Device:hasDPad() then
|
2016-05-26 06:09:49 +00:00
|
|
|
self.layout = self.buttons_layout
|
2022-03-04 20:20:00 +00:00
|
|
|
self:refocusWidget()
|
2015-09-13 08:09:00 +00:00
|
|
|
else
|
|
|
|
self.key_events = {} -- deregister all key press event listeners
|
|
|
|
end
|
2023-03-23 17:37:40 +00:00
|
|
|
if self.shrink_unneeded_width and table_min_needed_width ~= false
|
|
|
|
and table_min_needed_width > 0 and table_min_needed_width < self.width then
|
|
|
|
self.width = table_min_needed_width > self.shrink_min_width and table_min_needed_width or self.shrink_min_width
|
|
|
|
self.shrink_unneeded_width = false
|
|
|
|
self:free()
|
|
|
|
self:init()
|
|
|
|
end
|
2013-06-15 15:13:19 +00:00
|
|
|
end
|
|
|
|
|
2023-05-11 18:23:45 +00:00
|
|
|
function ButtonTable:addVerticalSpan()
|
|
|
|
table.insert(self.container, VerticalSpan:new{
|
|
|
|
width = Size.span.vertical_default,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
function ButtonTable:addVerticalSeparator(black_line)
|
|
|
|
table.insert(self.container, LineWidget:new{
|
|
|
|
background = black_line and Blitbuffer.COLOR_BLACK or Blitbuffer.COLOR_GRAY,
|
|
|
|
dimen = Geom:new{
|
|
|
|
w = self.width,
|
|
|
|
h = self.sep_width,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
function ButtonTable:setupGridScrollBehaviour()
|
|
|
|
-- So that the last row get the same height as all others,
|
|
|
|
-- we add an invisible separator below it
|
|
|
|
self.container:resetLayout()
|
|
|
|
table.insert(self.container, VerticalSpan:new{
|
|
|
|
width = self.sep_width,
|
|
|
|
})
|
|
|
|
self.container:getSize() -- have it recompute its offsets and size
|
|
|
|
|
|
|
|
-- Generate self.step_scroll_grid (so that what we add next is not part of it)
|
|
|
|
self:getStepScrollGrid()
|
|
|
|
|
|
|
|
-- Insert 2 lines off-dimensions in VerticalGroup (that will show only when overflowing)
|
|
|
|
table.insert(self.container, 1, LineWidget:new{
|
|
|
|
background = Blitbuffer.COLOR_BLACK,
|
|
|
|
dimen = Geom:new{
|
|
|
|
w = self.width,
|
|
|
|
h = self.sep_width,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
table.insert(self.container._offsets, 1, { x=self.width, y=-self.sep_width })
|
|
|
|
table.insert(self.container, LineWidget:new{
|
|
|
|
background = Blitbuffer.COLOR_BLACK,
|
|
|
|
dimen = Geom:new{
|
|
|
|
w = self.width,
|
|
|
|
h = self.sep_width,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
table.insert(self.container._offsets, { x=self.width, y=self.container._size.h + self.sep_width })
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
function ButtonTable:getStepScrollGrid()
|
|
|
|
if not self.step_scroll_grid then
|
|
|
|
local step_rows = {}
|
|
|
|
local offsets = self.container._offsets
|
|
|
|
local idx = self.zero_sep and 2 or 1
|
|
|
|
local row_num = 1
|
|
|
|
while idx <= #self.container do
|
|
|
|
local row = {
|
|
|
|
row_num = row_num, -- (not used, but may help with debugging)
|
|
|
|
top = offsets[idx].y, -- top of our vspan above text
|
|
|
|
content_top = offsets[idx+1].y, -- top of our text widget
|
|
|
|
content_bottom = offsets[idx+2].y - 1, -- bottom of our text widget
|
|
|
|
bottom = idx+4 <= #self.container and offsets[idx+4].y -1 or self.container:getSize().h -1
|
|
|
|
-- bottom of our vspan + separator below text
|
|
|
|
-- To implement when needed:
|
|
|
|
-- columns = { array of similar info about each button in that row's HorizontalGroup }
|
|
|
|
-- Its absence means free scrolling on the x-axis (if scroll ends up being needed)
|
2017-10-08 15:53:25 +00:00
|
|
|
}
|
2023-05-11 18:23:45 +00:00
|
|
|
table.insert(step_rows, row)
|
|
|
|
idx = idx + 4
|
|
|
|
row_num = row_num + 1
|
|
|
|
end
|
|
|
|
self.step_scroll_grid = step_rows
|
2017-10-08 15:53:25 +00:00
|
|
|
end
|
2023-05-11 18:23:45 +00:00
|
|
|
return self.step_scroll_grid
|
2015-09-13 08:09:00 +00:00
|
|
|
end
|
|
|
|
|
2018-08-06 19:16:30 +00:00
|
|
|
function ButtonTable:getButtonById(id)
|
|
|
|
return self.button_by_id[id] -- nil if not found
|
|
|
|
end
|
|
|
|
|
2013-10-18 20:38:07 +00:00
|
|
|
return ButtonTable
|