local BD = require ( " ui/bidi " )
local CenterContainer = require ( " ui/widget/container/centercontainer " )
local ConfirmBox = require ( " ui/widget/confirmbox " )
local Device = require ( " device " )
local Event = require ( " ui/event " )
local InputContainer = require ( " ui/widget/container/inputcontainer " )
local Screensaver = require ( " ui/screensaver " )
local UIManager = require ( " ui/uimanager " )
local logger = require ( " logger " )
local dbg = require ( " dbg " )
local util = require ( " util " )
local Screen = Device.screen
local _ = require ( " gettext " )
local T = require ( " ffi/util " ) . template
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.
2 years ago
local ReaderMenu = InputContainer : extend {
tab_item_table = nil ,
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.
2 years ago
menu_items = nil , -- table, mandatory
registered_widgets = nil , -- array
}
function ReaderMenu : init ( )
self.menu_items = {
[ " KOMenu:menu_buttons " ] = {
-- top menu
} ,
-- items in top menu
navi = {
icon = " appbar.navigation " ,
} ,
typeset = {
icon = " appbar.typeset " ,
} ,
setting = {
icon = " appbar.settings " ,
} ,
tools = {
icon = " appbar.tools " ,
} ,
search = {
icon = " appbar.search " ,
} ,
filemanager = {
icon = " appbar.filebrowser " ,
remember = false ,
callback = function ( )
self : onTapCloseMenu ( )
self.ui : onClose ( )
self.ui : showFileManager ( )
end ,
} ,
main = {
icon = " appbar.menu " ,
}
}
self.registered_widgets = { }
self : registerKeyEvents ( )
if G_reader_settings : has ( " activate_menu " ) then
self.activation_menu = G_reader_settings : readSetting ( " activate_menu " )
else
self.activation_menu = " swipe_tap "
end
-- delegate gesture listener to readerui, NOP our own
self.ges_events = nil
end
function ReaderMenu : onGesture ( ) end
function ReaderMenu : registerKeyEvents ( )
if Device : hasKeys ( ) then
if Device : isTouchDevice ( ) then
self.key_events . PressMenu = { { " Menu " } }
if Device : hasFewKeys ( ) then
self.key_events . PressMenu = { { { " Menu " , " Right " } } }
end
else
-- Map Menu key to top menu only, because the bottom menu is only designed for touch devices.
--- @fixme: Is this still the case?
--- (Swapping between top and bottom might not be implemented, though, so it might still be a good idea).
self.key_events . ShowMenu = { { " Menu " } }
if Device : hasFewKeys ( ) then
self.key_events . ShowMenu = { { { " Menu " , " Right " } } }
end
end
end
end
ReaderMenu.onPhysicalKeyboardConnected = ReaderMenu.registerKeyEvents
function ReaderMenu : getPreviousFile ( )
return require ( " readhistory " ) : getPreviousFile ( self.ui . document.file )
end
function ReaderMenu : initGesListener ( )
if not Device : isTouchDevice ( ) then return end
local DTAP_ZONE_MENU = G_defaults : readSetting ( " DTAP_ZONE_MENU " )
local DTAP_ZONE_MENU_EXT = G_defaults : readSetting ( " DTAP_ZONE_MENU_EXT " )
self.ui : registerTouchZones ( {
{
id = " readermenu_tap " ,
ges = " tap " ,
screen_zone = {
ratio_x = DTAP_ZONE_MENU.x , ratio_y = DTAP_ZONE_MENU.y ,
ratio_w = DTAP_ZONE_MENU.w , ratio_h = DTAP_ZONE_MENU.h ,
} ,
overrides = {
" tap_forward " ,
" tap_backward " ,
} ,
handler = function ( ges ) return self : onTapShowMenu ( ges ) end ,
} ,
{
id = " readermenu_ext_tap " ,
ges = " tap " ,
screen_zone = {
ratio_x = DTAP_ZONE_MENU_EXT.x , ratio_y = DTAP_ZONE_MENU_EXT.y ,
ratio_w = DTAP_ZONE_MENU_EXT.w , ratio_h = DTAP_ZONE_MENU_EXT.h ,
} ,
overrides = {
" readermenu_tap " ,
} ,
handler = function ( ges ) return self : onTapShowMenu ( ges ) end ,
} ,
{
id = " readermenu_swipe " ,
ges = " swipe " ,
screen_zone = {
ratio_x = DTAP_ZONE_MENU.x , ratio_y = DTAP_ZONE_MENU.y ,
ratio_w = DTAP_ZONE_MENU.w , ratio_h = DTAP_ZONE_MENU.h ,
} ,
overrides = {
" rolling_swipe " ,
" paging_swipe " ,
} ,
handler = function ( ges ) return self : onSwipeShowMenu ( ges ) end ,
} ,
{
id = " readermenu_ext_swipe " ,
ges = " swipe " ,
screen_zone = {
ratio_x = DTAP_ZONE_MENU_EXT.x , ratio_y = DTAP_ZONE_MENU_EXT.y ,
ratio_w = DTAP_ZONE_MENU_EXT.w , ratio_h = DTAP_ZONE_MENU_EXT.h ,
} ,
overrides = {
" readermenu_swipe " ,
} ,
handler = function ( ges ) return self : onSwipeShowMenu ( ges ) end ,
} ,
{
id = " readermenu_pan " ,
ges = " pan " ,
screen_zone = {
ratio_x = DTAP_ZONE_MENU.x , ratio_y = DTAP_ZONE_MENU.y ,
ratio_w = DTAP_ZONE_MENU.w , ratio_h = DTAP_ZONE_MENU.h ,
} ,
overrides = {
" rolling_pan " ,
" paging_pan " ,
} ,
handler = function ( ges ) return self : onSwipeShowMenu ( ges ) end ,
} ,
{
id = " readermenu_ext_pan " ,
ges = " pan " ,
screen_zone = {
ratio_x = DTAP_ZONE_MENU_EXT.x , ratio_y = DTAP_ZONE_MENU_EXT.y ,
ratio_w = DTAP_ZONE_MENU_EXT.w , ratio_h = DTAP_ZONE_MENU_EXT.h ,
} ,
overrides = {
" readermenu_pan " ,
} ,
handler = function ( ges ) return self : onSwipeShowMenu ( ges ) end ,
} ,
} )
end
ReaderMenu.onReaderReady = ReaderMenu.initGesListener
function ReaderMenu : setUpdateItemTable ( )
for _ , widget in pairs ( self.registered_widgets ) do
local ok , err = pcall ( widget.addToMainMenu , widget , self.menu_items )
if not ok then
logger.err ( " failed to register widget " , widget.name , err )
end
end
-- typeset tab
self.menu_items . document_settings = {
text = _ ( " Document settings " ) ,
sub_item_table = {
{
text = _ ( " Reset document settings to default " ) ,
keep_menu_open = true ,
callback = function ( )
UIManager : show ( ConfirmBox : new {
text = _ ( " Reset current document settings to their default values? \n \n Reading position, highlights and bookmarks will be kept. \n The document will be reloaded. " ) ,
ok_text = _ ( " Reset " ) ,
ok_callback = function ( )
local current_file = self.ui . document.file
self : onTapCloseMenu ( )
self.ui : onClose ( )
require ( " apps/filemanager/filemanagerutil " ) . resetDocumentSettings ( current_file )
require ( " apps/reader/readerui " ) : showReader ( current_file )
end ,
} )
end ,
} ,
{
text = _ ( " Save document settings as default " ) ,
keep_menu_open = true ,
callback = function ( )
UIManager : show ( ConfirmBox : new {
text = _ ( " Save current document settings as default values? " ) ,
ok_text = _ ( " Save " ) ,
ok_callback = function ( )
self : onTapCloseMenu ( )
self : saveDocumentSettingsAsDefault ( )
UIManager : show ( require ( " ui/widget/notification " ) : new {
text = _ ( " Default settings updated " ) ,
} )
end ,
} )
end ,
} ,
} ,
}
self.menu_items . page_overlap = require ( " ui/elements/page_overlap " )
-- settings tab
-- insert common settings
for id , common_setting in pairs ( dofile ( " frontend/ui/elements/common_settings_menu_table.lua " ) ) do
self.menu_items [ id ] = common_setting
end
if Device : isTouchDevice ( ) then
-- Settings > Taps & Gestures; mostly concerns touch related page turn stuff, and only applies to Reader
self.menu_items . page_turns = require ( " ui/elements/page_turns " )
end
-- Settings > Navigation; while also related to page turns, this mostly concerns physical keys, and applies *everywhere*
if Device : hasKeys ( ) then
self.menu_items . physical_buttons_setup = require ( " ui/elements/physical_buttons " )
end
-- insert DjVu render mode submenu just before the last entry (show advanced)
-- this is a bit of a hack
if self.ui . document.is_djvu then
self.menu_items . djvu_render_mode = self.view : getRenderModeMenuTable ( )
end
if Device : supportsScreensaver ( ) then
local ss_book_settings = {
text = _ ( " Do not show this book cover on sleep screen " ) ,
enabled_func = function ( )
if self.ui and self.ui . document then
local screensaverType = G_reader_settings : readSetting ( " screensaver_type " )
return screensaverType == " cover " or screensaverType == " disable "
else
return false
end
end ,
checked_func = function ( )
return self.ui and self.ui . doc_settings and self.ui . doc_settings : isTrue ( " exclude_screensaver " )
end ,
callback = function ( )
if Screensaver : isExcluded ( ) then
self.ui . doc_settings : makeFalse ( " exclude_screensaver " )
else
self.ui . doc_settings : makeTrue ( " exclude_screensaver " )
end
self.ui : saveSettings ( )
end ,
added_by_readermenu_flag = true ,
}
local screensaver_sub_item_table = require ( " ui/elements/screensaver_menu " )
-- Before inserting this new item, remove any previously added one
for i = # screensaver_sub_item_table , 1 , - 1 do
if screensaver_sub_item_table [ i ] . added_by_readermenu_flag then
table.remove ( screensaver_sub_item_table , i )
end
end
table.insert ( screensaver_sub_item_table , ss_book_settings )
self.menu_items . screensaver = {
text = _ ( " Sleep screen " ) ,
sub_item_table = screensaver_sub_item_table ,
}
end
local PluginLoader = require ( " pluginloader " )
self.menu_items . plugin_management = {
text = _ ( " Plugin management " ) ,
sub_item_table = PluginLoader : genPluginManagerSubItem ( )
}
-- main menu tab
-- insert common info
for id , common_setting in pairs ( dofile ( " frontend/ui/elements/common_info_menu_table.lua " ) ) do
self.menu_items [ id ] = common_setting
end
-- insert common exit for reader
for id , common_setting in pairs ( dofile ( " frontend/ui/elements/common_exit_menu_table.lua " ) ) do
self.menu_items [ id ] = common_setting
end
self.menu_items . open_previous_document = {
text_func = function ( )
local previous_file = self : getPreviousFile ( )
if not G_reader_settings : isTrue ( " open_last_menu_show_filename " ) or not previous_file then
return _ ( " Open previous document " )
end
local path , file_name = util.splitFilePathName ( previous_file ) -- luacheck: no unused
return T ( _ ( " Previous: %1 " ) , BD.filename ( file_name ) )
end ,
enabled_func = function ( )
return self : getPreviousFile ( ) ~= nil
end ,
callback = function ( )
self.ui : onOpenLastDoc ( )
end ,
hold_callback = function ( )
local previous_file = self : getPreviousFile ( )
UIManager : show ( ConfirmBox : new {
text = T ( _ ( " Would you like to open the previous document: %1? " ) , BD.filepath ( previous_file ) ) ,
ok_text = _ ( " OK " ) ,
ok_callback = function ( )
self.ui : switchDocument ( previous_file )
end ,
} )
end
}
local order = require ( " ui/elements/reader_menu_order " )
local MenuSorter = require ( " ui/menusorter " )
self.tab_item_table = MenuSorter : mergeAndSort ( " reader " , self.menu_items , order )
end
dbg : guard ( ReaderMenu , ' setUpdateItemTable ' ,
function ( self )
local mock_menu_items = { }
for _ , widget in pairs ( self.registered_widgets ) do
-- make sure addToMainMenu works in debug mode
widget : addToMainMenu ( mock_menu_items )
end
end )
function ReaderMenu : saveDocumentSettingsAsDefault ( )
local prefix
if self.ui . rolling then
G_reader_settings : saveSetting ( " cre_font " , self.ui . font.font_face )
G_reader_settings : saveSetting ( " copt_css " , self.ui . document.default_css )
G_reader_settings : saveSetting ( " style_tweaks " , self.ui . styletweak.global_tweaks )
prefix = " copt_ "
else
prefix = " kopt_ "
end
for k , v in pairs ( self.ui . document.configurable ) do
G_reader_settings : saveSetting ( prefix .. k , v )
end
end
function ReaderMenu : exitOrRestart ( callback , force )
if self.menu_container then self : onTapCloseMenu ( ) end
-- Only restart sets a callback, which suits us just fine for this check ;)
if callback and not force and not Device : isStartupScriptUpToDate ( ) then
UIManager : show ( ConfirmBox : new {
text = _ ( " KOReader's startup script has been updated. You'll need to completely exit KOReader to finalize the update. " ) ,
ok_text = _ ( " Restart anyway " ) ,
ok_callback = function ( )
self : exitOrRestart ( callback , true )
end ,
} )
return
end
UIManager : nextTick ( function ( )
self.ui : onClose ( )
if callback ~= nil then
-- show an empty widget so that the callback always happens
local Widget = require ( " ui/widget/widget " )
local widget = Widget : new {
width = Screen : getWidth ( ) ,
height = Screen : getHeight ( ) ,
}
UIManager : show ( widget )
local waiting = function ( waiting )
-- if we don't do this you can get a situation where either the
-- program won't exit due to remaining widgets until they're
-- dismissed or if the callback forces all widgets to close,
-- that the save document ConfirmBox is also closed
if self.ui and self.ui . document and self.ui . document : isEdited ( ) then
logger.dbg ( " waiting for save settings " )
UIManager : scheduleIn ( 1 , function ( ) waiting ( waiting ) end )
else
callback ( )
UIManager : close ( widget )
end
end
UIManager : scheduleIn ( 1 , function ( ) waiting ( waiting ) end )
end
end )
local FileManager = require ( " apps/filemanager/filemanager " )
if FileManager.instance then
FileManager.instance : onClose ( )
end
end
function ReaderMenu : onShowMenu ( tab_index )
if self.tab_item_table == nil then
self : setUpdateItemTable ( )
end
if not tab_index then
tab_index = self.last_tab_index
end
local menu_container = CenterContainer : new {
covers_header = true ,
ignore = " height " ,
dimen = Screen : getSize ( ) ,
}
local main_menu
if Device : isTouchDevice ( ) or Device : hasDPad ( ) then
local TouchMenu = require ( " ui/widget/touchmenu " )
main_menu = TouchMenu : new {
width = Screen : getWidth ( ) ,
last_index = tab_index ,
tab_item_table = self.tab_item_table ,
show_parent = menu_container ,
}
else
local Menu = require ( " ui/widget/menu " )
main_menu = Menu : new {
title = _ ( " Document menu " ) ,
item_table = Menu.itemTableFromTouchMenu ( self.tab_item_table ) ,
width = Screen : getWidth ( ) - 100 ,
show_parent = menu_container ,
}
end
ReaderUI: Saner FM/RD lifecycle
* Ensure that going from one to the other tears down the former and
its plugins before instantiating the latter and its plugins.
UIManager: Unify Event sending & broadcasting
* Make the two behave the same way (walk the widget stack from top to
bottom), and properly handle the window stack shrinking shrinking
*and* growing.
Previously, broadcasting happened bottom-to-top and didn't really
handle the list shrinking/growing, while sending only handled the list
shrinking by a single element, and hopefully that element being the one
the event was just sent to.
These two items combined allowed us to optimize suboptimal
refresh behavior with Menu and other Menu classes when
opening/closing a document.
e.g., the "opening document" Notification is now properly regional,
and the "open last doc" option no longer flashes like a crazy person
anymore.
Plugins: Allow optimizing Menu refresh with custom menus, too.
Requires moving Menu's close_callback *after* onMenuSelect, which, eh,
probably makes sense, and is probably harmless in the grand scheme of
things.
3 years ago
main_menu.close_callback = function ( )
self : onCloseReaderMenu ( )
end
main_menu.touch_menu_callback = function ( )
self.ui : handleEvent ( Event : new ( " CloseConfigMenu " ) )
end
menu_container [ 1 ] = main_menu
-- maintain a reference to menu_container
self.menu_container = menu_container
UIManager : show ( menu_container )
return true
end
function ReaderMenu : onCloseReaderMenu ( )
if not self.menu_container then return true end
self.last_tab_index = self.menu_container [ 1 ] . last_index
self : onSaveSettings ( )
UIManager : close ( self.menu_container )
self.menu_container = nil
return true
end
function ReaderMenu : onSetDimensions ( dimen )
-- This widget doesn't support in-place layout updates, so, close & reopen
if self.menu_container then
self : onCloseReaderMenu ( )
self : onShowMenu ( )
end
-- update gesture zones according to new screen dimen
-- (On CRe, this will get called a second time by ReaderReady once the document is reloaded).
self : initGesListener ( )
end
function ReaderMenu : onCloseDocument ( )
if Device : supportsScreensaver ( ) then
-- Remove the item we added (which cleans up references to document
-- and doc_settings embedded in functions)
local screensaver_sub_item_table = require ( " ui/elements/screensaver_menu " )
for i = # screensaver_sub_item_table , 1 , - 1 do
if screensaver_sub_item_table [ i ] . added_by_readermenu_flag then
table.remove ( screensaver_sub_item_table , i )
end
end
end
end
function ReaderMenu : _getTabIndexFromLocation ( ges )
if self.tab_item_table == nil then
self : setUpdateItemTable ( )
end
if not ges then
return self.last_tab_index
-- if the start position is far right
elseif ges.pos . x > Screen : getWidth ( ) * ( 2 / 3 ) then
return BD.mirroredUILayout ( ) and 1 or # self.tab_item_table
-- if the start position is far left
elseif ges.pos . x < Screen : getWidth ( ) * ( 1 / 3 ) then
return BD.mirroredUILayout ( ) and # self.tab_item_table or 1
-- if center return the last index
else
return self.last_tab_index
end
end
function ReaderMenu : onSwipeShowMenu ( ges )
if self.activation_menu ~= " tap " and ges.direction == " south " then
if G_reader_settings : nilOrTrue ( " show_bottom_menu " ) then
self.ui : handleEvent ( Event : new ( " ShowConfigMenu " ) )
end
self : onShowMenu ( self : _getTabIndexFromLocation ( ges ) )
self.ui : handleEvent ( Event : new ( " HandledAsSwipe " ) ) -- cancel any pan scroll made
return true
end
end
function ReaderMenu : onTapShowMenu ( ges )
if self.activation_menu ~= " swipe " then
if G_reader_settings : nilOrTrue ( " show_bottom_menu " ) then
self.ui : handleEvent ( Event : new ( " ShowConfigMenu " ) )
end
self : onShowMenu ( self : _getTabIndexFromLocation ( ges ) )
return true
end
end
function ReaderMenu : onPressMenu ( )
if G_reader_settings : nilOrTrue ( " show_bottom_menu " ) then
self.ui : handleEvent ( Event : new ( " ShowConfigMenu " ) )
end
self : onShowMenu ( )
return true
end
function ReaderMenu : onTapCloseMenu ( )
self : onCloseReaderMenu ( )
self.ui : handleEvent ( Event : new ( " CloseConfigMenu " ) )
end
function ReaderMenu : onReadSettings ( config )
self.last_tab_index = config : readSetting ( " readermenu_tab_index " ) or 1
end
function ReaderMenu : onSaveSettings ( )
self.ui . doc_settings : saveSetting ( " readermenu_tab_index " , self.last_tab_index )
end
function ReaderMenu : onMenuSearch ( )
self : onShowMenu ( )
self.menu_container [ 1 ] : onShowMenuSearch ( )
end
function ReaderMenu : registerToMainMenu ( widget )
table.insert ( self.registered_widgets , widget )
end
return ReaderMenu