2
0
mirror of https://github.com/koreader/koreader synced 2024-10-31 21:20:20 +00:00
koreader/frontend/ui/uimanager.lua

651 lines
25 KiB
Lua
Raw Normal View History

2013-10-18 20:38:07 +00:00
local Device = require("ui/device")
local Screen = require("ui/screen")
local Input = require("ui/input")
local Event = require("ui/event")
2013-10-22 18:51:29 +00:00
local DEBUG = require("dbg")
2013-10-24 13:45:02 +00:00
local _ = require("gettext")
2014-06-04 09:22:45 +00:00
local util = require("ffi/util")
2014-08-14 20:18:27 +00:00
local ImageWidget = require("ui/widget/imagewidget")
2012-04-22 19:29:48 +00:00
2013-02-02 06:36:29 +00:00
-- initialize output module, this must be initialized before Input
Screen:init()
-- initialize the input handling
2012-04-22 19:29:48 +00:00
Input:init()
-- NOTE: Those have been confirmed on Kindle devices. Might be completely different on Kobo (except for AUTO)!
local WAVEFORM_MODE_INIT = 0x0 -- Screen goes to white (clears)
local WAVEFORM_MODE_DU = 0x1 -- Grey->white/grey->black
local WAVEFORM_MODE_GC16 = 0x2 -- High fidelity (flashing)
local WAVEFORM_MODE_GC4 = WAVEFORM_MODE_GC16 -- For compatibility
local WAVEFORM_MODE_GC16_FAST = 0x3 -- Medium fidelity
local WAVEFORM_MODE_A2 = 0x4 -- Faster but even lower fidelity
local WAVEFORM_MODE_GL16 = 0x5 -- High fidelity from white transition
local WAVEFORM_MODE_GL16_FAST = 0x6 -- Medium fidelity from white transition
2014-07-06 16:43:50 +00:00
-- Kindle FW >= 5.3
local WAVEFORM_MODE_DU4 = 0x7 -- Medium fidelity 4 level of gray direct update
2014-07-06 16:43:50 +00:00
-- Kindle PW2
local WAVEFORM_MODE_REAGL = 0x8 -- Ghost compensation waveform
local WAVEFORM_MODE_REAGLD = 0x9 -- Ghost compensation waveform with dithering
2014-07-06 16:43:50 +00:00
local WAVEFORM_MODE_AUTO = 0x101
2012-04-22 19:29:48 +00:00
-- there is only one instance of this
2013-10-18 20:38:07 +00:00
local UIManager = {
2014-03-13 13:52:43 +00:00
default_refresh_type = 0, -- 0 for partial refresh, 1 for full refresh
default_waveform_mode = WAVEFORM_MODE_GC16, -- high fidelity waveform
fast_waveform_mode = WAVEFORM_MODE_A2,
full_refresh_waveform_mode = WAVEFORM_MODE_GC16,
partial_refresh_waveform_mode = WAVEFORM_MODE_GC16,
2014-03-13 13:52:43 +00:00
-- force to repaint all the widget is stack, will be reset to false
-- after each ui loop
repaint_all = false,
-- force to do full refresh, will be reset to false
-- after each ui loop
full_refresh = false,
2014-07-06 19:38:13 +00:00
-- force to do partial refresh, will be reset to false
2014-03-13 13:52:43 +00:00
-- after each ui loop
2014-07-06 19:38:13 +00:00
partial_refresh = false,
2014-03-13 13:52:43 +00:00
-- trigger a full refresh when counter reaches FULL_REFRESH_COUNT
FULL_REFRESH_COUNT = G_reader_settings:readSetting("full_refresh_count") or DRCOUNTMAX,
2014-03-13 13:52:43 +00:00
refresh_count = 0,
2013-03-12 17:18:53 +00:00
event_handlers = nil,
2014-03-13 13:52:43 +00:00
_running = true,
_window_stack = {},
_execution_stack = {},
_dirty = {},
_zeromqs = {},
2014-08-14 20:18:27 +00:00
suspend_msg = nil
2012-04-22 19:29:48 +00:00
}
function UIManager:getPicture(file)
local contentopf
local contentpath
local epub_folder = "temp/epub"
local function check_extension(cover)
if cover then
local itype = string.lower(string.match(cover, ".+%.([^.]+)") or "")
if not (itype == "png" or itype == "jpg" or itype == "jpeg" or itype == "tiff") then
cover = nil
end
end
return cover
end
local function getValue(zipfile,which_line,to_find,excludecheck)
local f = io.open(zipfile,"r")
local i
if f then
local line = f:read()
while line and not i do
i = line:lower():find(which_line:lower()) -- found something
if i then
f.close()
line = line:match(to_find .. "\"([^\"]+)\".*")
if not excludecheck then line = check_extension(line) end
if line then
return line
else
i = nil
end
end
if not i then line = f:read() end
end
f.close()
end
end
local function guess(extension)
local cover = contentpath .. "Images/cover." .. extension
pcall(os.execute("unzip \"" .. file .. "\" \"" .. cover .. "\" -oq -d " .. epub_folder))
cover = epub_folder .. "/" .. cover
if not io.open(cover,"r") then
cover = nil
end
return cover
end
local function try_content_opf(which_line,to_find,addimage)
local cover = getValue(epub_folder .. "/" .. contentopf,which_line,to_find)
local imageadd
if cover then
if addimage then
imageadd = "Images/"
else
imageadd = ""
end
cover = contentpath .. imageadd .. cover
pcall(os.execute("unzip \"" .. file .. "\" \"" .. cover .. "\" -oq -d " .. epub_folder))
cover = epub_folder .. "/" .. cover
if not io.open(cover,"r") then cover = nil end
end
return check_extension(cover)
end
local cover
if file then
pcall(lfs.mkdir("temp"))
pcall(os.execute("rm -rf " .. epub_folder))
pcall(lfs.mkdir(epub_folder))
pcall(os.execute("unzip \"" .. file .. "\" cover.jpeg -oq -d " .. epub_folder))
if io.open(epub_folder .. "/cover.jpeg","r") then -- picture in main folder
cover = epub_folder .. "/cover.jpeg" -- found one
else
pcall(os.execute("unzip \"" .. file .. "\" \"META-INF/container.xml\" -oq -d " .. epub_folder)) -- read container.xml
contentopf = getValue(epub_folder .. "/META-INF/container.xml","^%s*<rootfile ","full[-]path=",true)
if contentopf then
contentpath = contentopf:match("(.*)[/][^/]+")
if contentpath then
contentpath = contentpath .. "/"
else
contentpath = ""
end
pcall(os.execute("unzip \"" .. file .. "\" \"" .. contentopf .. "\" -oq -d " .. epub_folder)) -- read content.opf
cover = try_content_opf("^%s*<meta name=\"cover\"","content=",true) -- Make Room
if not cover then cover = try_content_opf('id="cover',"item href=",false) end -- Kishon
if not cover then cover = try_content_opf("cover","href=",true) end
if not cover then cover = try_content_opf("cover","=",true) end
if not cover then cover = try_content_opf("cover","=",false) end
if not cover then guess("jpg") end
if not cover then guess("jpeg") end
if not cover then guess("png") end
end
end
end
return check_extension(cover)
end
2014-08-14 20:18:27 +00:00
function UIManager:getRandomPicture(dir)
local pics = {}
local i = 0
math.randomseed( os.time() )
for entry in lfs.dir(dir) do
if lfs.attributes(dir .. entry, "mode") == "file" then
local extension = string.lower(string.match(entry, ".+%.([^.]+)") or "")
if extension == "jpg" or extension == "jpeg" or extension == "png" then
i = i + 1
pics[i] = entry
end
end
end
return pics[math.random(i)]
end
function UIManager:init()
self.event_handlers = {
__default__ = function(input_event)
self:sendEvent(input_event)
end,
SaveState = function()
self:sendEvent(Event:new("FlushSettings"))
end
}
if Device:isKobo() then
-- lazy create suspend_msg to avoid dependence loop
2014-08-14 20:18:27 +00:00
function kobo_power(input_event)
2014-08-14 20:18:27 +00:00
if (input_event == "Power" or input_event == "Suspend")
and not Device.screen_saver_mode then
2014-08-14 20:18:27 +00:00
if not UIManager.suspend_msg then
local file
if KOBO_SCREEN_SAVER_LAST_BOOK then
file = self:getPicture(G_reader_settings:readSetting("lastfile"))
end
if type(KOBO_SCREEN_SAVER) == "string" or file then
if not file then
file = KOBO_SCREEN_SAVER
if lfs.attributes(file, "mode") == "directory" then
if string.sub(file,string.len(file)) ~= "/" then
file = file .. "/"
end
local dummy = self:getRandomPicture(file)
if dummy then file = file .. dummy end
2014-08-14 20:18:27 +00:00
end
end
if lfs.attributes(file, "mode") == "file" then
UIManager.suspend_msg = ImageWidget:new{
file = file,
width = Screen:getWidth(),
height = Screen:getHeight(),
}
end
end
if not UIManager.suspend_msg then
local InfoMessage = require("ui/widget/infomessage")
UIManager.suspend_msg = InfoMessage:new{ text = _("Suspended") }
end
end
if KOBO_LIGHT_OFF_ON_SUSPEND then Device:getPowerDevice():setIntensity(0) end
2014-08-14 20:18:27 +00:00
self:show(UIManager.suspend_msg)
self:sendEvent(Event:new("FlushSettings"))
Device:prepareSuspend()
self:scheduleIn(2, function() Device:Suspend() end)
elseif (input_event == "Power" or input_event == "Resume")
and Device.screen_saver_mode then
Device:Resume()
self:sendEvent(Event:new("Resume"))
2014-08-14 20:18:27 +00:00
if UIManager.suspend_msg then
self:close(UIManager.suspend_msg)
UIManager.suspend_msg = nil
end
if KOBO_LIGHT_ON_START and tonumber(KOBO_LIGHT_ON_START) > -1 then
Device:getPowerDevice():setIntensity( math.max( math.min(KOBO_LIGHT_ON_START,100) ,0) )
end
end
end
2014-08-14 20:18:27 +00:00
self.event_handlers["Power"] = kobo_power
self.event_handlers["Suspend"] = kobo_power
self.event_handlers["Resume"] = kobo_power
self.event_handlers["Light"] = function()
Device:getPowerDevice():toggleFrontlight()
end
self.event_handlers["__default__"] = function(input_event)
if Device.screen_saver_mode then
-- Suspension in Kobo can be interrupted by screen updates. We
-- ignore user touch input here so screen udpate won't be
-- triggered in suspend mode
return
else
self:sendEvent(input_event)
end
end
if KOBO_LIGHT_ON_START and tonumber(KOBO_LIGHT_ON_START) > -1 then
Device:getPowerDevice():setIntensity( math.max( math.min(KOBO_LIGHT_ON_START,100) ,0) )
end
elseif Device:isKindle() then
self.event_handlers["IntoSS"] = function()
self:sendEvent(Event:new("FlushSettings"))
Device:intoScreenSaver()
end
self.event_handlers["OutOfSS"] = function()
Device:outofScreenSaver()
end
self.event_handlers["Charging"] = function()
Device:usbPlugIn()
end
self.event_handlers["NotCharging"] = function()
Device:usbPlugOut()
self:sendEvent(Event:new("NotCharging"))
end
-- Emulate the stock reader refresh behavior...
--[[
NOTE: For ref, on a Touch (debugPaint is my new best friend):
UI: gc16_fast
Reader: When flash: if to/from img: gc16, else gc16_fast; when non-flash: auto (seems to prefer gl16_fast); Waiting for marker only on flash
On a PW2:
Same as Touch, except reader uses reagl on non-flash, non-flash lasts longer (12 pgs); Always waits for marker
--]]
-- We don't really have an easy way to know if we're refreshing the UI, or a page, or if said page contains an image, so go with the highest fidelity option
self.full_refresh_waveform_mode = WAVEFORM_MODE_GC16
-- We spend much more time in the reader than the UI, and our UI isn't very graphic anyway, so go with the reader behavior
if Device:getModel() == "KindlePaperWhite2" then
self.partial_refresh_waveform_mode = WAVEFORM_MODE_REAGL
else
self.partial_refresh_waveform_mode = WAVEFORM_MODE_GL16_FAST
-- NOTE: Or we could go back to what KOReader did before fa55acc in koreader-base, which was also use WAVEFORM_MODE_AUTO ;). I have *no* idea how the driver makes its choice though...
--self.partial_refresh_waveform_mode = WAVEFORM_MODE_AUTO
end
end
end
2012-04-22 19:29:48 +00:00
-- register & show a widget
function UIManager:show(widget, x, y)
2014-03-13 13:52:43 +00:00
-- put widget on top of stack
table.insert(self._window_stack, {x = x or 0, y = y or 0, widget = widget})
-- and schedule it to be painted
self:setDirty(widget)
-- tell the widget that it is shown now
widget:handleEvent(Event:new("Show"))
-- check if this widget disables double tap gesture
if widget.disable_double_tap then
Input.disable_double_tap = true
end
2012-04-22 19:29:48 +00:00
end
-- unregister a widget
function UIManager:close(widget)
2014-03-13 13:52:43 +00:00
Input.disable_double_tap = DGESDETECT_DISABLE_DOUBLE_TAP
local dirty = false
for i = #self._window_stack, 1, -1 do
if self._window_stack[i].widget == widget then
table.remove(self._window_stack, i)
dirty = true
elseif self._window_stack[i].widget.disable_double_tap then
Input.disable_double_tap = true
end
end
if dirty then
-- schedule remaining widgets to be painted
for i = 1, #self._window_stack do
self:setDirty(self._window_stack[i].widget)
end
end
2012-04-22 19:29:48 +00:00
end
-- schedule an execution task
function UIManager:schedule(time, action)
2014-03-13 13:52:43 +00:00
table.insert(self._execution_stack, { time = time, action = action })
2012-04-22 19:29:48 +00:00
end
-- schedule task in a certain amount of seconds (fractions allowed) from now
function UIManager:scheduleIn(seconds, action)
2014-03-13 13:52:43 +00:00
local when = { util.gettime() }
local s = math.floor(seconds)
local usecs = (seconds - s) * 1000000
when[1] = when[1] + s
when[2] = when[2] + usecs
if when[2] > 1000000 then
when[1] = when[1] + 1
when[2] = when[2] - 1000000
end
self:schedule(when, action)
2012-04-22 19:29:48 +00:00
end
-- register a widget to be repainted
function UIManager:setDirty(widget, refresh_type)
2014-03-13 13:52:43 +00:00
-- "auto": request full refresh
-- "full": force full refresh
-- "partial": partial refresh
if not refresh_type then
refresh_type = "auto"
end
self._dirty[widget] = refresh_type
2012-04-22 19:29:48 +00:00
end
function UIManager:insertZMQ(zeromq)
table.insert(self._zeromqs, zeromq)
return zeromq
end
function UIManager:removeZMQ(zeromq)
for i = #self._zeromqs, 1, -1 do
if self._zeromqs[i] == zeromq then
table.remove(self._zeromqs, i)
end
end
end
-- set full refresh rate for e-ink screen
-- and make the refresh rate persistant in global reader settings
function UIManager:setRefreshRate(rate)
DEBUG("set screen full refresh rate", rate)
self.FULL_REFRESH_COUNT = rate
G_reader_settings:saveSetting("full_refresh_count", rate)
end
-- get full refresh rate for e-ink screen
function UIManager:getRefreshRate(rate)
return self.FULL_REFRESH_COUNT
end
2012-04-22 19:29:48 +00:00
-- signal to quit
function UIManager:quit()
2014-03-13 13:52:43 +00:00
self._running = false
for i = #self._zeromqs, 1, -1 do
self._zeromqs[i]:stop()
table.remove(self._zeromqs, i)
end
2012-04-22 19:29:48 +00:00
end
-- transmit an event to registered widgets
function UIManager:sendEvent(event)
if #self._window_stack == 0 then return end
2014-03-13 13:52:43 +00:00
-- top level widget has first access to the event
if self._window_stack[#self._window_stack].widget:handleEvent(event) then
return
end
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- if the event is not consumed, active widgets can access it
for _, widget in ipairs(self._window_stack) do
if widget.widget.is_always_active then
if widget.widget:handleEvent(event) then return end
end
if widget.widget.active_widgets then
for _, active_widget in ipairs(widget.widget.active_widgets) do
if active_widget:handleEvent(event) then return end
end
end
end
2012-04-22 19:29:48 +00:00
end
function UIManager:checkTasks()
2014-03-13 13:52:43 +00:00
local now = { util.gettime() }
2014-03-13 13:52:43 +00:00
-- check if we have timed events in our queue and search next one
local wait_until = nil
local all_tasks_checked
repeat
all_tasks_checked = true
for i = #self._execution_stack, 1, -1 do
local task = self._execution_stack[i]
if not task.time
or task.time[1] < now[1]
or task.time[1] == now[1] and task.time[2] < now[2] then
-- task is pending to be executed right now. do it.
task.action()
-- and remove from table
table.remove(self._execution_stack, i)
-- start loop again, since new tasks might be on the
-- queue now
all_tasks_checked = false
elseif not wait_until
or wait_until[1] > task.time[1]
or wait_until[1] == task.time[1] and wait_until[2] > task.time[2] then
-- task is to be run in the future _and_ is scheduled
-- earlier than the tasks we looked at already
-- so adjust to the currently examined task instead.
wait_until = task.time
end
end
until all_tasks_checked
return wait_until
end
2012-04-22 19:29:48 +00:00
-- this is the main loop of the UI controller
-- it is intended to manage input events and delegate
-- them to dialogs
function UIManager:run()
2014-03-13 13:52:43 +00:00
self._running = true
while self._running do
local now = { util.gettime() }
local wait_until = self:checkTasks()
2013-03-12 17:18:53 +00:00
2014-03-13 13:52:43 +00:00
--DEBUG("---------------------------------------------------")
--DEBUG("exec stack", self._execution_stack)
--DEBUG("window stack", self._window_stack)
--DEBUG("dirty stack", self._dirty)
--DEBUG("---------------------------------------------------")
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- stop when we have no window to show
if #self._window_stack == 0 then
DEBUG("no dialog left to show")
self:quit()
2014-03-13 13:52:43 +00:00
return nil
end
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- repaint dirty widgets
local dirty = false
local request_full_refresh = false
local force_full_refresh = false
2014-07-06 19:38:13 +00:00
local force_partial_refresh = false
2014-03-13 13:52:43 +00:00
local force_fast_refresh = false
for _, widget in ipairs(self._window_stack) do
if self.repaint_all or self._dirty[widget.widget] then
widget.widget:paintTo(Screen.bb,
2014-07-16 11:51:09 +00:00
widget.x + Screen:offsetX(),
widget.y + Screen:offsetY())
2014-03-13 13:52:43 +00:00
if self._dirty[widget.widget] == "auto" then
request_full_refresh = true
end
if self._dirty[widget.widget] == "full" then
force_full_refresh = true
end
if self._dirty[widget.widget] == "partial" then
2014-07-06 19:38:13 +00:00
force_partial_refresh = true
2014-03-13 13:52:43 +00:00
end
if self._dirty[widget.widget] == "fast" then
force_fast_refresh = true
end
-- and remove from list after painting
self._dirty[widget.widget] = nil
-- trigger repaint
dirty = true
end
end
2014-06-04 09:22:45 +00:00
2014-03-13 13:52:43 +00:00
if self.full_refresh then
dirty = true
force_full_refresh = true
end
2014-07-06 19:38:13 +00:00
if self.partial_refresh then
2014-03-13 13:52:43 +00:00
dirty = true
2014-07-06 19:38:13 +00:00
force_partial_refresh = true
2014-03-13 13:52:43 +00:00
end
2014-06-04 09:22:45 +00:00
2014-03-13 13:52:43 +00:00
self.repaint_all = false
self.full_refresh = false
2014-07-06 19:38:13 +00:00
self.partial_refresh = false
2014-06-04 09:22:45 +00:00
2014-03-13 13:52:43 +00:00
local refresh_type = self.default_refresh_type
local waveform_mode = self.default_waveform_mode
if dirty then
2014-07-06 19:38:13 +00:00
if force_partial_refresh or force_fast_refresh then
2014-03-13 13:52:43 +00:00
refresh_type = 0
elseif force_full_refresh or self.refresh_count == self.FULL_REFRESH_COUNT - 1 then
refresh_type = 1
end
-- Handle the waveform mode selection...
if refresh_type == 1 then
waveform_mode = self.full_refresh_waveform_mode
else
waveform_mode = self.partial_refresh_waveform_mode
end
2014-03-13 13:52:43 +00:00
if force_fast_refresh then
waveform_mode = self.fast_waveform_mode
end
if self.update_region_func then
local update_region = self.update_region_func()
-- in some rare cases update region has 1 pixel offset
2014-06-04 09:22:45 +00:00
Screen:refresh(refresh_type, waveform_mode,
2014-03-13 13:52:43 +00:00
update_region.x-1, update_region.y-1,
update_region.w+2, update_region.h+2)
else
Screen:refresh(refresh_type, waveform_mode)
end
if self.refresh_type == 1 then
self.refresh_count = 0
2014-07-06 19:38:13 +00:00
elseif not force_partial_refresh and not force_full_refresh then
2014-03-13 13:52:43 +00:00
self.refresh_count = (self.refresh_count + 1)%self.FULL_REFRESH_COUNT
end
self.update_region_func = nil
end
2013-03-12 17:18:53 +00:00
2014-03-13 13:52:43 +00:00
self:checkTasks()
2013-03-12 17:18:53 +00:00
2014-03-13 13:52:43 +00:00
-- wait for next event
-- note that we will skip that if in the meantime we have tasks that are ready to run
local input_event = nil
if not wait_until then
if #self._zeromqs > 0 then
-- pending message queue, wait 100ms for input
input_event = Input:waitEvent(1000*100)
if not input_event or input_event.handler == "onInputError" then
for _, zeromq in ipairs(self._zeromqs) do
input_event = zeromq:waitEvent()
if input_event then break end
end
end
else
-- no pending task, wait endlessly
input_event = Input:waitEvent()
end
2014-03-13 13:52:43 +00:00
elseif wait_until[1] > now[1]
or wait_until[1] == now[1] and wait_until[2] > now[2] then
local wait_for = { s = wait_until[1] - now[1], us = wait_until[2] - now[2] }
if wait_for.us < 0 then
wait_for.s = wait_for.s - 1
wait_for.us = 1000000 + wait_for.us
end
-- wait until next task is pending
input_event = Input:waitEvent(wait_for.us, wait_for.s)
end
2012-04-22 19:29:48 +00:00
2014-03-13 13:52:43 +00:00
-- delegate input_event to handler
if input_event then
local handler = self.event_handlers[input_event]
if handler then
handler(input_event)
2014-03-13 13:52:43 +00:00
else
self.event_handlers["__default__"](input_event)
2014-03-13 13:52:43 +00:00
end
end
end
2012-04-22 19:29:48 +00:00
end
2013-10-18 20:38:07 +00:00
function UIManager:getRefreshMenuTable()
local function custom_1() return G_reader_settings:readSetting("refresh_rate_1") or 12 end
local function custom_2() return G_reader_settings:readSetting("refresh_rate_2") or 22 end
local function custom_3() return G_reader_settings:readSetting("refresh_rate_3") or 99 end
local function custom_input(name)
return {
title = _("Input page number for a full refresh"),
type = "number",
hint = "(1 - 99)",
callback = function(input)
local rate = tonumber(input)
G_reader_settings:saveSetting(name, rate)
UIManager:setRefreshRate(rate)
end,
}
end
return {
text = _("E-ink full refresh rate"),
sub_item_table = {
{
text = _("Every page"),
checked_func = function() return UIManager:getRefreshRate() == 1 end,
callback = function() UIManager:setRefreshRate(1) end,
},
{
text = _("Every 6 pages"),
checked_func = function() return UIManager:getRefreshRate() == 6 end,
callback = function() UIManager:setRefreshRate(6) end,
},
{
text_func = function() return _("Custom ") .. "1: " .. custom_1() .. _(" pages") end,
checked_func = function() return UIManager:getRefreshRate() == custom_1() end,
callback = function() UIManager:setRefreshRate(custom_1()) end,
hold_input = custom_input("refresh_rate_1")
},
{
text_func = function() return _("Custom ") .. "2: " .. custom_2() .. _(" pages") end,
checked_func = function() return UIManager:getRefreshRate() == custom_2() end,
callback = function() UIManager:setRefreshRate(custom_2()) end,
hold_input = custom_input("refresh_rate_2")
},
{
text_func = function() return _("Custom ") .. "3: " .. custom_3() .. _(" pages") end,
checked_func = function() return UIManager:getRefreshRate() == custom_3() end,
callback = function() UIManager:setRefreshRate(custom_3()) end,
hold_input = custom_input("refresh_rate_3")
},
}
}
end
UIManager:init()
2013-10-18 20:38:07 +00:00
return UIManager