UI Behavior tweaks (#3983)

* Switch all initial highlights to "fast" update

i.e., everything that does an invert
Plus a few other things that refresh small UI elements onTap
Re #3130

* Tweak refreshtype for a number of widgets:
  * Fix iconbutton dimen
  * Make touchmenu flash on close & initial menu popup. Full-screen on close.
  * Use flashing updates when opening/closing dictionary popup. Full-screen on close.
  * Switch FileManager to partial.
    It's mostly text, and we want flash promotion there.
  * Make configdialog & menu flash on exit
  * Make FLWidget flash on close
  * virtualkeyboard: flash on layout change & popup.
  * Potentially not that great workaround to ensure we actually see the
highlights in the FM's chevrons
  * Flash when closing BookStatus Widget
  * Optimize away a quirk of the dual "fast" update in touchmenu

* Promote updates to flashing slightly more agressively.

* Document what each refreshtype actually does.

With a few guidelines on their optimal usecases.

* Switch remaining scheduleIn(0.0) to nextTick()

* Tighter scheduling timers

Shaving a hundred ms off UI callbacks...

* Cache FFI C Library namespace

* Ask MuPDF to convert pixmaps to BGR on Kobo

Fix #3949

* Mention koxtoolchain in the README

re #3972

* Kindle: Handle *all* fonts via EXT_FONT_DIR instead of bind mounts insanity

* Make black flashes in UI elements user-configurable

(All or nothing).

* Jot down some random KOA2 sysfs path
pull/3993/head
NiLuJe 6 years ago committed by GitHub
parent bdbaa7dfdd
commit 5871132c25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,7 +14,7 @@
KOReader is a document viewer application, originally created for Kindle KOReader is a document viewer application, originally created for Kindle
e-ink readers. It currently runs on Kindle, Kobo, PocketBook, Ubuntu Touch e-ink readers. It currently runs on Kindle, Kobo, PocketBook, Ubuntu Touch
and Android devices. Developers can also run a KOReader emulator and Android devices. Developers can also run a KOReader emulator
for development purposes on desktop PCs with Linux, Windows and for development purposes on desktop PCs with Linux, Windows and
Mac OSX. Mac OSX.
Main features for users Main features for users
@ -104,6 +104,12 @@ block you from building for Kobo or Kindle. Remove them if you get an ld error,
`/usr/lib/gcc-cross/arm-linux-gnueabihf/4.8/../../../../arm-linux-gnueabihf/bin/ `/usr/lib/gcc-cross/arm-linux-gnueabihf/4.8/../../../../arm-linux-gnueabihf/bin/
ld: cannot find -lglib-2.0` ld: cannot find -lglib-2.0`
**NOTE:** In the specific case of Kindle & Kobo targets, while we make some effort to support these Linaro/Ubuntu TCs,
they do *not* exactly target the proper devices. While your build will go fine, this may lead to runtime failure.
As time goes by, and/or the more bleeding-edge your distro is, the greater the risk for mismatch gets.
Thankfully, we have a distribution-agnostic solution for you: [koxtoolchain](https://github.com/koreader/koxtoolchain)!
This will allow you to build the *exact* same TCs used to build the nightlies, thanks to the magic of [crosstool-ng](https://github.com/crosstool-ng/crosstool-ng).
On Mac OS X you may need to install the following tools using [Homebrew](https://brew.sh/): On Mac OS X you may need to install the following tools using [Homebrew](https://brew.sh/):
``` ```
brew install nasm binutils libtool autoconf automake cmake makedepend sdl2 lua51 gettext pkg-config wget md5sha1sum brew install nasm binutils libtool autoconf automake cmake makedepend sdl2 lua51 gettext pkg-config wget md5sha1sum

@ -1 +1 @@
Subproject commit 1b7e584a27a460dc8375ad910cd2b92161776da9 Subproject commit 24150115ab25c5021176247a241caaed838718d4

@ -161,7 +161,7 @@ function FileManager:init()
function file_chooser:onPathChanged(path) -- luacheck: ignore function file_chooser:onPathChanged(path) -- luacheck: ignore
FileManager.instance.path_text:setText(truncatePath(filemanagerutil.abbreviate(path))) FileManager.instance.path_text:setText(truncatePath(filemanagerutil.abbreviate(path)))
UIManager:setDirty(FileManager.instance, function() UIManager:setDirty(FileManager.instance, function()
return "ui", FileManager.instance.path_text.dimen return "partial", FileManager.instance.path_text.dimen
end) end)
return true return true
end end

@ -1,6 +1,7 @@
local Generic = require("device/generic/device") local Generic = require("device/generic/device")
local _, android = pcall(require, "android") local _, android = pcall(require, "android")
local ffi = require("ffi") local ffi = require("ffi")
local C = ffi.C
local logger = require("logger") local logger = require("logger")
local function yes() return true end local function yes() return true end
@ -26,11 +27,11 @@ function Device:init()
event_map = require("device/android/event_map"), event_map = require("device/android/event_map"),
handleMiscEv = function(this, ev) handleMiscEv = function(this, ev)
logger.dbg("Android application event", ev.code) logger.dbg("Android application event", ev.code)
if ev.code == ffi.C.APP_CMD_SAVE_STATE then if ev.code == C.APP_CMD_SAVE_STATE then
return "SaveState" return "SaveState"
elseif ev.code == ffi.C.APP_CMD_GAINED_FOCUS then elseif ev.code == C.APP_CMD_GAINED_FOCUS then
this.device.screen:refreshFull() this.device.screen:refreshFull()
elseif ev.code == ffi.C.APP_CMD_WINDOW_REDRAW_NEEDED then elseif ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then
this.device.screen:refreshFull() this.device.screen:refreshFull()
end end
end, end,
@ -47,13 +48,13 @@ function Device:init()
-- check if we have a keyboard -- check if we have a keyboard
if android.lib.AConfiguration_getKeyboard(android.app.config) if android.lib.AConfiguration_getKeyboard(android.app.config)
== ffi.C.ACONFIGURATION_KEYBOARD_QWERTY == C.ACONFIGURATION_KEYBOARD_QWERTY
then then
self.hasKeyboard = yes self.hasKeyboard = yes
end end
-- check if we have a touchscreen -- check if we have a touchscreen
if android.lib.AConfiguration_getTouchscreen(android.app.config) if android.lib.AConfiguration_getTouchscreen(android.app.config)
~= ffi.C.ACONFIGURATION_TOUCHSCREEN_NOTOUCH ~= C.ACONFIGURATION_TOUCHSCREEN_NOTOUCH
then then
self.isTouchDevice = yes self.isTouchDevice = yes
end end

@ -26,6 +26,7 @@ local Device = {
needsTouchScreenProbe = no, needsTouchScreenProbe = no,
hasClipboard = no, hasClipboard = no,
hasColorScreen = no, hasColorScreen = no,
hasBGRFrameBuffer = no,
-- use these only as a last resort. We should abstract the functionality -- use these only as a last resort. We should abstract the functionality
-- and have device dependent implementations in the corresponting -- and have device dependent implementations in the corresponting
@ -65,6 +66,8 @@ function Device:init()
return self.screen.isColorScreen() return self.screen.isColorScreen()
end end
self.screen.isBGRFrameBuffer = self.hasBGRFrameBuffer
local is_eink = G_reader_settings:readSetting("eink") local is_eink = G_reader_settings:readSetting("eink")
self.screen.eink = (is_eink == nil) or is_eink self.screen.eink = (is_eink == nil) or is_eink

@ -248,6 +248,10 @@ local KindleOasis2 = Kindle:new{
hasFrontlight = yes, hasFrontlight = yes,
display_dpi = 300, display_dpi = 300,
touch_dev = "/dev/input/by-path/platform-30a30000.i2c-event", touch_dev = "/dev/input/by-path/platform-30a30000.i2c-event",
-- NOTE: Incomplete, but at least they're confirmed.
--batt_capacity_file = "/sys/class/power_supply/max77796-battery/capacity",
--is_charging_file = "/sys/class/power_supply/max77796-charger/charging",
} }
local KindleBasic2 = Kindle:new{ local KindleBasic2 = Kindle:new{

@ -23,6 +23,7 @@ local Kobo = Generic:new{
model = "Kobo", model = "Kobo",
isKobo = yes, isKobo = yes,
isTouchDevice = yes, -- all of them are isTouchDevice = yes, -- all of them are
hasBGRFrameBuffer = yes, -- has always been the case, even on 16bpp FWs
-- most Kobos have X/Y switched for the touch screen -- most Kobos have X/Y switched for the touch screen
touch_switch_xy = true, touch_switch_xy = true,

@ -7,6 +7,7 @@ local Geom = require("ui/geometry")
local RenderImage = require("ui/renderimage") local RenderImage = require("ui/renderimage")
local Screen = require("device").screen local Screen = require("device").screen
local ffi = require("ffi") local ffi = require("ffi")
local C = ffi.C
local lfs = require("libs/libkoreader-lfs") local lfs = require("libs/libkoreader-lfs")
local logger = require("logger") local logger = require("logger")
@ -205,7 +206,7 @@ function CreDocument:getCoverPageImage()
local data, size = self._document:getCoverPageImageData() local data, size = self._document:getCoverPageImageData()
if data and size then if data and size then
local image = RenderImage:renderImageData(data, size) local image = RenderImage:renderImageData(data, size)
ffi.C.free(data) -- free the userdata we got from crengine C.free(data) -- free the userdata we got from crengine
return image return image
end end
end end
@ -215,7 +216,7 @@ function CreDocument:getImageFromPosition(pos, want_frames)
if data and size then if data and size then
logger.dbg("CreDocument: got image data from position", data, size) logger.dbg("CreDocument: got image data from position", data, size)
local image = RenderImage:renderImageData(data, size, want_frames) local image = RenderImage:renderImageData(data, size, want_frames)
ffi.C.free(data) -- free the userdata we got from crengine C.free(data) -- free the userdata we got from crengine
return image return image
end end
end end

@ -1,12 +1,16 @@
local Cache = require("cache") local Cache = require("cache")
local CacheItem = require("cacheitem") local CacheItem = require("cacheitem")
local Device = require("device")
local Document = require("document/document") local Document = require("document/document")
local DrawContext = require("ffi/drawcontext") local DrawContext = require("ffi/drawcontext")
local KoptOptions = require("ui/data/koptoptions") local KoptOptions = require("ui/data/koptoptions")
local logger = require("logger") local logger = require("logger")
local util = require("util") local util = require("util")
local ffi = require("ffi")
local C = ffi.C
local pdf = nil local pdf = nil
local PdfDocument = Document:new{ local PdfDocument = Document:new{
_document = false, _document = false,
is_pdf = true, is_pdf = true,
@ -25,6 +29,12 @@ function PdfDocument:init()
-- and :postRenderPage() when mupdf is called without kopt involved. -- and :postRenderPage() when mupdf is called without kopt involved.
pdf.color = false pdf.color = false
self:updateColorRendering() self:updateColorRendering()
if pdf.bgr == nil then
pdf.bgr = false
if Device:hasBGRFrameBuffer() then
pdf.bgr = true
end
end
self.koptinterface = require("document/koptinterface") self.koptinterface = require("document/koptinterface")
self.configurable:loadDefaults(self.options) self.configurable:loadDefaults(self.options)
local ok local ok
@ -134,7 +144,6 @@ end
function PdfDocument:saveHighlight(pageno, item) function PdfDocument:saveHighlight(pageno, item)
self.is_edited = true self.is_edited = true
local ffi = require("ffi")
-- will also need mupdf_h.lua to be evaluated once -- will also need mupdf_h.lua to be evaluated once
-- but this is guaranteed at this point -- but this is guaranteed at this point
local n = #item.pboxes local n = #item.pboxes
@ -152,13 +161,13 @@ function PdfDocument:saveHighlight(pageno, item)
quadpoints[8*i-1] = item.pboxes[i].y quadpoints[8*i-1] = item.pboxes[i].y
end end
local page = self._document:openPage(pageno) local page = self._document:openPage(pageno)
local annot_type = ffi.C.PDF_ANNOT_HIGHLIGHT local annot_type = C.PDF_ANNOT_HIGHLIGHT
if item.drawer == "lighten" then if item.drawer == "lighten" then
annot_type = ffi.C.PDF_ANNOT_HIGHLIGHT annot_type = C.PDF_ANNOT_HIGHLIGHT
elseif item.drawer == "underscore" then elseif item.drawer == "underscore" then
annot_type = ffi.C.PDF_ANNOT_UNDERLINE annot_type = C.PDF_ANNOT_UNDERLINE
elseif item.drawer == "strikeout" then elseif item.drawer == "strikeout" then
annot_type = ffi.C.PDF_ANNOT_STRIKEOUT annot_type = C.PDF_ANNOT_STRIKEOUT
end end
page:addMarkupAnnotation(quadpoints, n, annot_type) page:addMarkupAnnotation(quadpoints, n, annot_type)
page:close() page:close()

@ -0,0 +1,12 @@
local _ = require("gettext")
return {
text = _("Avoid mandatory black flashes in UI"),
checked_func = function()
return G_reader_settings:isTrue("avoid_flashing_ui")
end,
callback = function()
G_reader_settings:flipNilOrFalse("avoid_flashing_ui")
end,
}

@ -113,6 +113,7 @@ common_settings.screen = {
require("ui/elements/screen_disable_double_tap_table"), require("ui/elements/screen_disable_double_tap_table"),
require("ui/elements/flash_ui"), require("ui/elements/flash_ui"),
require("ui/elements/flash_keyboard"), require("ui/elements/flash_keyboard"),
require("ui/elements/avoid_flashing_ui"),
}, },
} }
if Screen.isColorScreen() then if Screen.isColorScreen() then

@ -5,6 +5,7 @@ local MessageQueue = require("ui/message/messagequeue")
local _ = require("ffi/zeromq_h") local _ = require("ffi/zeromq_h")
local zmq = ffi.load("libs/libzmq.so.4") local zmq = ffi.load("libs/libzmq.so.4")
local czmq = ffi.load("libs/libczmq.so.1") local czmq = ffi.load("libs/libczmq.so.1")
local C = ffi.C
local StreamMessageQueue = MessageQueue:new{ local StreamMessageQueue = MessageQueue:new{
host = nil, host = nil,
@ -13,7 +14,7 @@ local StreamMessageQueue = MessageQueue:new{
function StreamMessageQueue:start() function StreamMessageQueue:start()
self.context = czmq.zctx_new(); self.context = czmq.zctx_new();
self.socket = czmq.zsocket_new(self.context, ffi.C.ZMQ_STREAM) self.socket = czmq.zsocket_new(self.context, C.ZMQ_STREAM)
self.poller = czmq.zpoller_new(self.socket, nil) self.poller = czmq.zpoller_new(self.socket, nil)
local endpoint = string.format("tcp://%s:%d", self.host, self.port) local endpoint = string.format("tcp://%s:%d", self.host, self.port)
logger.warn("connect to endpoint", endpoint) logger.warn("connect to endpoint", endpoint)
@ -24,7 +25,7 @@ function StreamMessageQueue:start()
local id_size = ffi.new("size_t[1]", 256) local id_size = ffi.new("size_t[1]", 256)
local buffer = ffi.new("uint8_t[?]", id_size[0]) local buffer = ffi.new("uint8_t[?]", id_size[0])
-- @todo: check return of zmq_getsockopt -- @todo: check return of zmq_getsockopt
zmq.zmq_getsockopt(self.socket, ffi.C.ZMQ_IDENTITY, buffer, id_size) zmq.zmq_getsockopt(self.socket, C.ZMQ_IDENTITY, buffer, id_size)
self.id = ffi.string(buffer, id_size[0]) self.id = ffi.string(buffer, id_size[0])
logger.dbg("id", #self.id, self.id) logger.dbg("id", #self.id, self.id)
end end

@ -3,6 +3,7 @@ Image rendering module.
]] ]]
local ffi = require("ffi") local ffi = require("ffi")
local Device = require("device")
local logger = require("logger") local logger = require("logger")
-- Will be loaded when needed -- Will be loaded when needed
@ -66,6 +67,13 @@ end
-- @treturn BlitBuffer -- @treturn BlitBuffer
function RenderImage:renderImageDataWithMupdf(data, size, width, height) function RenderImage:renderImageDataWithMupdf(data, size, width, height)
if not Mupdf then Mupdf = require("ffi/mupdf") end if not Mupdf then Mupdf = require("ffi/mupdf") end
-- NOTE: Kobo's fb is BGR, not RGB. Handle the conversion in MuPDF if needed.
if Mupdf.bgr == nil then
Mupdf.bgr = false
if Device:hasBGRFrameBuffer() then
Mupdf.bgr = true
end
end
local ok, image = pcall(Mupdf.renderImage, data, size, width, height) local ok, image = pcall(Mupdf.renderImage, data, size, width, height)
logger.dbg("Mupdf.renderImage", ok, image) logger.dbg("Mupdf.renderImage", ok, image)
if not ok then if not ok then

@ -185,7 +185,7 @@ Modal widget should be always on top.
For refreshtype & refreshregion see description of setDirty(). For refreshtype & refreshregion see description of setDirty().
]] ]]
---- @param widget a widget object ---- @param widget a widget object
---- @param refreshtype "full", "partial", "ui", "fast" ---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion a Geom object ---- @param refreshregion a Geom object
---- @int x ---- @int x
---- @int y ---- @int y
@ -226,12 +226,12 @@ Unregisters a widget.
For refreshtype & refreshregion see description of setDirty(). For refreshtype & refreshregion see description of setDirty().
]] ]]
---- @param widget a widget object ---- @param widget a widget object
---- @param refreshtype "full", "partial", "ui", "fast" ---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion a Geom object ---- @param refreshregion a Geom object
---- @see setDirty ---- @see setDirty
function UIManager:close(widget, refreshtype, refreshregion) function UIManager:close(widget, refreshtype, refreshregion)
if not widget then if not widget then
logger.dbg("widget not exist to be closed") logger.dbg("widget to be closed does not exist")
return return
end end
logger.dbg("close widget", widget.id or widget.name) logger.dbg("close widget", widget.id or widget.name)
@ -242,7 +242,7 @@ function UIManager:close(widget, refreshtype, refreshregion)
widget:handleEvent(Event:new("CloseWidget")) widget:handleEvent(Event:new("CloseWidget"))
-- make it disabled by default and check any widget that enables it -- make it disabled by default and check any widget that enables it
Input.disable_double_tap = true Input.disable_double_tap = true
-- then remove all reference to that widget on stack and update -- then remove all references to that widget on stack and refresh
for i = #self._window_stack, 1, -1 do for i = #self._window_stack, 1, -1 do
if self._window_stack[i].widget == widget then if self._window_stack[i].widget == widget then
table.remove(self._window_stack, i) table.remove(self._window_stack, i)
@ -322,6 +322,17 @@ function UIManager:nextTick(action)
return self:scheduleIn(0, action) return self:scheduleIn(0, action)
end end
-- Useful to run UI callbacks ASAP without skipping repaints
function UIManager:tickAfterNext(action)
return self:nextTick(function() self:nextTick(action) end)
end
--[[
-- NOTE: This appears to work *nearly* just as well, but does sometimes go too fast (might depend on kernel HZ & NO_HZ settings?)
function UIManager:tickAfterNext(action)
return self:scheduleIn(0.001, action)
end
--]]
--[[-- Unschedules an execution task. --[[-- Unschedules an execution task.
In order to unschedule anonymous functions, store a reference. In order to unschedule anonymous functions, store a reference.
@ -349,6 +360,38 @@ the second parameter (refreshtype) can either specify a refreshtype
(optionally in combination with a refreshregion - which is suggested) (optionally in combination with a refreshregion - which is suggested)
or a function that returns refreshtype AND refreshregion and is called or a function that returns refreshtype AND refreshregion and is called
after painting the widget. after painting the widget.
Here's a quick rundown of what each refreshtype should be used for:
full: high-fidelity flashing refresh (f.g., large images).
Highest quality, but highest latency.
Don't abuse if you only want a flash (in this case, prefer flashpartial or flashui).
partial: medium fidelity refresh (f.g., text on a white background).
Can be promoted to flashing after FULL_REFRESH_COUNT refreshes.
Don't abuse to avoid spurious flashes.
ui: medium fidelity refresh (f.g., mixed content).
Should apply to most UI elements.
fast: low fidelity refresh (f.g., monochrome content).
Should apply to most highlighting effects achieved through inversion.
Note that if your highlighted element contains text,
you might want to keep the unhighlight refresh as "ui" instead, for crisper text.
(Or optimize that refresh away entirely, if you can get away with it).
flashui: like ui, but flashing.
Can be used when showing a UI element for the first time, to avoid ghosting.
flashpartial: like partial, but flashing (and not counting towards flashing promotions).
Can be used when closing an UI element, to avoid ghosting.
You can even drop the region in these cases, to ensure a fullscreen flash.
NOTE: On REAGL devices, "flashpartial" will NOT actually flash (by design).
As such, even onClose, you might prefer "flashui" in some rare instances.
NOTE: You'll notice a trend on UI elements that are usually shown *over* some kind of text
of using "ui" onShow & onUpdate, but "partial" onClose.
This is by design: "partial" is what the reader uses, as it's tailor-made for pure text
over a white background, so this ensures we resume the usual flow of the reader.
The same dynamic is true for their flashing counterparts, in the rare instances we enforce flashes.
Any kind of "partial" refresh *will* count towards a flashing promotion after FULL_REFRESH_COUNT refreshes,
so making sure your stuff only applies to the proper region is key to avoiding spurious large black flashes.
That said, depending on your use case, using "ui" onClose can be a perfectly valid decision, and will ensure
never seeing a flash because of that widget.
@usage @usage
@ -358,7 +401,7 @@ UIManager:setDirty(self.widget, function() return "ui", self.someelement.dimen e
--]] --]]
---- @param widget a widget object ---- @param widget a widget object
---- @param refreshtype "full", "partial", "ui", "fast" ---- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast"
---- @param refreshregion a Geom object ---- @param refreshregion a Geom object
function UIManager:setDirty(widget, refreshtype, refreshregion) function UIManager:setDirty(widget, refreshtype, refreshregion)
if widget then if widget then
@ -375,9 +418,21 @@ function UIManager:setDirty(widget, refreshtype, refreshregion)
if type(refreshtype) == "function" then if type(refreshtype) == "function" then
-- callback, will be issued after painting -- callback, will be issued after painting
table.insert(self._refresh_func_stack, refreshtype) table.insert(self._refresh_func_stack, refreshtype)
if dbg.is_on then
-- FIXME: We can't consume the return values of refreshtype by running it, because for a reason that is beyond me (scoping? gc?), that renders it useless later, meaning we then enqueue refreshes with bogus arguments...
-- Thankfully, we can track them in _refresh()'s logging very soon after that...
logger.dbg("setDirty via a func from widget", widget and (widget.name or widget.id or tostring(widget)))
end
else else
-- otherwise, enqueue refresh -- otherwise, enqueue refresh
self:_refresh(refreshtype, refreshregion) self:_refresh(refreshtype, refreshregion)
if dbg.is_on then
if refreshregion then
logger.dbg("setDirty", refreshtype and refreshtype or "nil", "from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil", "w/ region", refreshregion.x, refreshregion.y, refreshregion.w, refreshregion.h)
else
logger.dbg("setDirty", refreshtype and refreshtype or "nil", "from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil", "w/ NO region")
end
end
end end
end end
dbg:guard(UIManager, 'setDirty', dbg:guard(UIManager, 'setDirty',
@ -395,6 +450,18 @@ dbg:guard(UIManager, 'setDirty',
end end
end) end)
-- Clear the full repaint & refreshes queues.
-- NOTE: Beware! This doesn't take any prisonners!
-- You shouldn't have to resort to this unless in very specific circumstances!
-- plugins/coverbrowser.koplugin/covermenu.lua building a franken-menu out of buttondialogtitle & buttondialog
-- and wanting to avoid inheriting their original paint/refresh cycle being a prime example.
function UIManager:clearRenderStack()
logger.dbg("clearRenderStack: Clearing the full render stack!")
self._dirty = {}
self._refresh_func_stack = {}
self._refresh_stack = {}
end
function UIManager:insertZMQ(zeromq) function UIManager:insertZMQ(zeromq)
table.insert(self._zeromqs, zeromq) table.insert(self._zeromqs, zeromq)
return zeromq return zeromq
@ -540,12 +607,14 @@ function UIManager:_checkTasks()
end end
-- precedence of refresh modes: -- precedence of refresh modes:
local refresh_modes = { fast = 1, ui = 2, partial = 3, full = 4 } local refresh_modes = { fast = 1, ui = 2, partial = 3, flashui = 4, flashpartial = 5, full = 6 }
-- refresh methods in framebuffer implementation -- refresh methods in framebuffer implementation
local refresh_methods = { local refresh_methods = {
fast = "refreshFast", fast = "refreshFast",
ui = "refreshUI", ui = "refreshUI",
partial = "refreshPartial", partial = "refreshPartial",
flashui = "refreshFlashUI",
flashpartial = "refreshFlashPartial",
full = "refreshFull", full = "refreshFull",
} }
@ -556,6 +625,7 @@ Will return the mode that takes precedence.
--]] --]]
local function update_mode(mode1, mode2) local function update_mode(mode1, mode2)
if refresh_modes[mode1] > refresh_modes[mode2] then if refresh_modes[mode1] > refresh_modes[mode2] then
logger.dbg("update_mode: Update refresh mode", mode2, "to", mode1)
return mode1 return mode1
else else
return mode2 return mode2
@ -569,7 +639,7 @@ Widgets call this in their paintTo() method in order to notify
UIManager that a certain part of the screen is to be refreshed. UIManager that a certain part of the screen is to be refreshed.
@param mode @param mode
refresh mode ("full", "partial", "ui", "fast") refresh mode ("full", "flashpartial", "flashui", "partial", "ui", "fast")
@param region @param region
Rect() that specifies the region to be updated Rect() that specifies the region to be updated
optional, update will affect whole screen if not specified. optional, update will affect whole screen if not specified.
@ -580,16 +650,40 @@ function UIManager:_refresh(mode, region)
if not region and mode == "full" then if not region and mode == "full" then
self.refresh_count = 0 -- reset counter on explicit full refresh self.refresh_count = 0 -- reset counter on explicit full refresh
end end
-- special case: full screen partial update -- Handle downgrading flashing modes to non-flashing modes, according to user settings.
-- will get promoted every self.FULL_REFRESH_COUNT updates -- NOTE: Do it before "full" promotion and collision checks/update_mode.
if G_reader_settings:isTrue("avoid_flashing_ui") then
if mode == "flashui" then
mode = "ui"
logger.dbg("_refresh: downgraded flashui refresh to", mode)
elseif mode == "flashpartial" then
mode = "partial"
logger.dbg("_refresh: downgraded flashpartial refresh to", mode)
elseif mode == "partial" and region then
mode = "ui"
logger.dbg("_refresh: downgraded regional partial refresh to", mode)
end
end
-- special case: "partial" refreshes
-- will get promoted every self.FULL_REFRESH_COUNT refreshes
-- since _refresh can be called mutiple times via setDirty called in -- since _refresh can be called mutiple times via setDirty called in
-- different widget before a real screen repaint, we should make sure -- different widgets before a real screen repaint, we should make sure
-- refresh_count is incremented by only once at most for each repaint -- refresh_count is incremented by only once at most for each repaint
if not region and mode == "partial" and not self.refresh_counted then -- NOTE: Ideally, we'd only check for "partial"" w/ no region set (that neatly narrows it down to just the reader).
-- In practice, we also want to promote refreshes in a few other places, except purely text-poor UI elements.
-- (Putting "ui" in that list is problematic with a number of UI elements, most notably, ReaderHighlight,
-- because it is implemented as "ui" over the full viewport, since we can't devise a proper bounding box).
-- So we settle for only "partial", but treating full-screen ones slightly differently.
if mode == "partial" and not self.refresh_counted then
self.refresh_count = (self.refresh_count + 1) % self.FULL_REFRESH_COUNT self.refresh_count = (self.refresh_count + 1) % self.FULL_REFRESH_COUNT
if self.refresh_count == self.FULL_REFRESH_COUNT - 1 then if self.refresh_count == self.FULL_REFRESH_COUNT - 1 then
logger.dbg("promote refresh to full refresh") -- NOTE: Promote to "full" if no region (reader), to "flashui" otherwise (UI)
mode = "full" if region then
mode = "flashui"
else
mode = "full"
end
logger.dbg("_refresh: promote refresh to", mode)
end end
self.refresh_counted = true self.refresh_counted = true
end end
@ -597,20 +691,27 @@ function UIManager:_refresh(mode, region)
-- if no region is specified, define default region -- if no region is specified, define default region
region = region or Geom:new{w=Screen:getWidth(), h=Screen:getHeight()} region = region or Geom:new{w=Screen:getWidth(), h=Screen:getHeight()}
-- NOTE: While, ideally, we shouldn't merge refreshes w/ different waveform modes,
-- this allows us to optimize away a number of quirks of our rendering stack
-- (f.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms),
-- as well as a few actually effective merges
-- (f.g., the disappearance of a selection HL with the following menu update).
for i = 1, #self._refresh_stack do for i = 1, #self._refresh_stack do
-- check for collision with updates that are already enqueued -- check for collision with refreshes that are already enqueued
if region:intersectWith(self._refresh_stack[i].region) then if region:intersectWith(self._refresh_stack[i].region) then
-- combine both refreshes' regions -- combine both refreshes' regions
local combined = region:combine(self._refresh_stack[i].region) local combined = region:combine(self._refresh_stack[i].region)
-- update the mode, if needed -- update the mode, if needed
mode = update_mode(mode, self._refresh_stack[i].mode) mode = update_mode(mode, self._refresh_stack[i].mode)
-- remove colliding update -- remove colliding refresh
table.remove(self._refresh_stack, i) table.remove(self._refresh_stack, i)
-- and try again with combined data -- and try again with combined data
return self:_refresh(mode, combined) return self:_refresh(mode, combined)
end end
end end
-- if we hit no (more) collides, enqueue the update
-- if we've stopped hitting collisions, enqueue the refresh
logger.dbg("_refresh: Enqueued", mode, "update for region", region.x, region.y, region.w, region.h)
table.insert(self._refresh_stack, {mode = mode, region = region}) table.insert(self._refresh_stack, {mode = mode, region = region})
end end

@ -245,7 +245,7 @@ function BookStatusWidget:setStar(num)
table.insert(self.stars_container, stars_group) table.insert(self.stars_container, stars_group)
UIManager:setDirty(nil, "partial") UIManager:setDirty(nil, "ui")
return true return true
end end
@ -527,11 +527,11 @@ function BookStatusWidget:generateSwitchGroup(width)
end end
function BookStatusWidget:onConfigChoose(values, name, event, args, events, position) function BookStatusWidget:onConfigChoose(values, name, event, args, events, position)
UIManager:scheduleIn(0.05, function() UIManager:tickAfterNext(function()
if values then if values then
self:onChangeBookStatus(args, position) self:onChangeBookStatus(args, position)
end end
UIManager:setDirty("all") UIManager:setDirty("all", "ui")
end) end)
end end
@ -542,7 +542,8 @@ end
function BookStatusWidget:onClose() function BookStatusWidget:onClose()
self:saveSummary() self:saveSummary()
UIManager:setDirty("all") -- NOTE: Flash on close to avoid ghosting, since we show an image.
UIManager:setDirty("all", "flashpartial")
UIManager:close(self) UIManager:close(self)
return true return true
end end

@ -191,17 +191,17 @@ function Button:onTapSelectButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
UIManager:scheduleIn(0.0, function() -- NOTE: Flag all widgets as dirty to force a repaint, so we actually get to see the highlight.
self[1].invert = true -- (For some reason (wrong widget passed to setDirty?), we never saw the effects on the FM chevrons without this hack).
UIManager:setDirty(self.show_parent, function() self[1].invert = true
return "ui", self[1].dimen UIManager:setDirty("all", function()
end) return "fast", self[1].dimen
end) end)
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self[1].invert = false self[1].invert = false
UIManager:setDirty(self.show_parent, function() UIManager:setDirty("all", function()
return "ui", self[1].dimen return "fast", self[1].dimen
end) end)
end) end)
end end

@ -78,7 +78,6 @@ function ButtonProgressWidget:update()
UIManager:setDirty(self.show_parrent, function() UIManager:setDirty(self.show_parrent, function()
return "ui", self.dimen return "ui", self.dimen
end) end)
UIManager:setDirty("all")
end end
function ButtonProgressWidget:setPosition(position) function ButtonProgressWidget:setPosition(position)

@ -94,17 +94,15 @@ function CheckButton:onTapCheckButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
UIManager:scheduleIn(0.0, function() self.invert = true
self.invert = true UIManager:setDirty(self.show_parent, function()
UIManager:setDirty(self.show_parent, function() return "fast", self.dimen
return "ui", self.dimen
end)
end) end)
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self.invert = false self.invert = false
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen return "fast", self.dimen
end) end)
end) end)
end end
@ -130,14 +128,14 @@ end
function CheckButton:check() function CheckButton:check()
self:initCheckButton(true) self:initCheckButton(true)
UIManager:setDirty(self.parent, function() UIManager:setDirty(self.parent, function()
return "partial", self.dimen return "fast", self.dimen
end) end)
end end
function CheckButton:unCheck() function CheckButton:unCheck()
self:initCheckButton(false) self:initCheckButton(false)
UIManager:setDirty(self.parent, function() UIManager:setDirty(self.parent, function()
return "partial", self.dimen return "fast", self.dimen
end) end)
end end

@ -81,7 +81,7 @@ function OptionTextItem:onTapSelect()
self.event, self.args, self.event, self.args,
self.events, self.current_item) self.events, self.current_item)
UIManager:setDirty(self.config, function() UIManager:setDirty(self.config, function()
return "ui", self[1].dimen return "fast", self[1].dimen
end) end)
return true return true
end end
@ -144,7 +144,7 @@ function OptionIconItem:onTapSelect()
self.event, self.args, self.event, self.args,
self.events, self.current_item) self.events, self.current_item)
UIManager:setDirty(self.config, function() UIManager:setDirty(self.config, function()
return "ui", self[1].dimen return "fast", self[1].dimen
end) end)
return true return true
end end
@ -455,7 +455,7 @@ function ConfigOption:init()
num_buttons = #self.options[c].values, num_buttons = #self.options[c].values,
position = self.options[c].default_pos, position = self.options[c].default_pos,
callback = function(arg) callback = function(arg)
UIManager:scheduleIn(0.05, function() UIManager:tickAfterNext(function()
self.config:onConfigChoice(self.options[c].name, self.options[c].values[arg]) self.config:onConfigChoice(self.options[c].name, self.options[c].values[arg])
self.config:onConfigEvent(self.options[c].event, self.options[c].args[arg]) self.config:onConfigEvent(self.options[c].event, self.options[c].args[arg])
UIManager:setDirty("all") UIManager:setDirty("all")
@ -723,6 +723,7 @@ function ConfigDialog:update()
end end
function ConfigDialog:onCloseWidget() function ConfigDialog:onCloseWidget()
-- NOTE: As much as we would like to flash here, don't, because of adverse interactions with touchmenu that might lead to a double flash...
UIManager:setDirty("all", function() UIManager:setDirty("all", function()
return "partial", self.dialog_frame.dimen return "partial", self.dialog_frame.dimen
end) end)
@ -732,6 +733,8 @@ function ConfigDialog:onShowConfigPanel(index)
self.panel_index = index self.panel_index = index
local old_dimen = self.dialog_frame.dimen and self.dialog_frame.dimen:copy() local old_dimen = self.dialog_frame.dimen and self.dialog_frame.dimen:copy()
self:update() self:update()
-- NOTE: Keep that one as UI to avoid delay when both this and the topmenu are shown.
-- Plus, this is also called for each tab anyway, so that wouldn't have been great.
UIManager:setDirty("all", function() UIManager:setDirty("all", function()
local refresh_dimen = local refresh_dimen =
old_dimen and old_dimen:combine(self.dialog_frame.dimen) old_dimen and old_dimen:combine(self.dialog_frame.dimen)
@ -761,7 +764,7 @@ function ConfigDialog:onConfigEvents(option_events, arg_index)
end end
function ConfigDialog:onConfigChoose(values, name, event, args, events, position) function ConfigDialog:onConfigChoose(values, name, event, args, events, position)
UIManager:scheduleIn(0.05, function() UIManager:tickAfterNext(function()
if values then if values then
self:onConfigChoice(name, values[position]) self:onConfigChoice(name, values[position])
end end

@ -552,9 +552,9 @@ function DictQuickLookup:update()
self.movable, self.movable,
} }
UIManager:setDirty("all", function() UIManager:setDirty("all", function()
local update_region = self.dict_frame.dimen:combine(orig_dimen) local update_region = self.dict_frame and self.dict_frame.dimen and self.dict_frame.dimen:combine(orig_dimen) or orig_dimen
logger.dbg("update dict region", update_region) logger.dbg("update dict region", update_region)
return "ui", update_region return "partial", update_region
end) end)
end end
@ -576,15 +576,16 @@ function DictQuickLookup:onCloseWidget()
end end
end end
end end
-- NOTE: Drop region to make it a full-screen flash
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.dict_frame.dimen return "flashui", nil
end) end)
return true return true
end end
function DictQuickLookup:onShow() function DictQuickLookup:onShow()
UIManager:setDirty(self, function() UIManager:setDirty(self, function()
return "ui", self.dict_frame.dimen return "flashui", self.dict_frame.dimen
end) end)
return true return true
end end
@ -774,7 +775,7 @@ function DictQuickLookup:onSwipe(arg, ges)
self:changeToPrevDict() self:changeToPrevDict()
else else
if self.refresh_callback then self.refresh_callback() end if self.refresh_callback then self.refresh_callback() end
-- trigger full refresh -- trigger a full-screen HQ flashing refresh
UIManager:setDirty(nil, "full") UIManager:setDirty(nil, "full")
-- a long diagonal swipe may also be used for taking a screenshot, -- a long diagonal swipe may also be used for taking a screenshot,
-- so let it propagate -- so let it propagate

@ -7,6 +7,7 @@ local UIManager = require("ui/uimanager")
local ffi = require("ffi") local ffi = require("ffi")
local lfs = require("libs/libkoreader-lfs") local lfs = require("libs/libkoreader-lfs")
local util = require("ffi/util") local util = require("ffi/util")
local C = ffi.C
local _ = require("gettext") local _ = require("gettext")
local Screen = Device.screen local Screen = Device.screen
local getFileNameSuffix = require("util").getFileNameSuffix local getFileNameSuffix = require("util").getFileNameSuffix
@ -18,7 +19,7 @@ int strcoll (const char *str1, const char *str2);
-- string sort function respecting LC_COLLATE -- string sort function respecting LC_COLLATE
local function strcoll(str1, str2) local function strcoll(str1, str2)
return ffi.C.strcoll(str1, str2) < 0 return C.strcoll(str1, str2) < 0
end end
local function kobostrcoll(str1, str2) local function kobostrcoll(str1, str2)

@ -82,7 +82,7 @@ function FocusManager:onFocusMove(args)
-- we found a different object to focus -- we found a different object to focus
current_item:handleEvent(Event:new("Unfocus")) current_item:handleEvent(Event:new("Unfocus"))
self.layout[self.selected.y][self.selected.x]:handleEvent(Event:new("Focus")) self.layout[self.selected.y][self.selected.x]:handleEvent(Event:new("Focus"))
-- trigger a fast repaint, this seem to not count toward a fullscreen eink resfresh -- trigger a fast repaint, this does not count toward a flashing eink resfresh
-- TODO: is this really needed? -- TODO: is this really needed?
UIManager:setDirty(self.show_parent or self, "fast") UIManager:setDirty(self.show_parent or self, "fast")
break break

@ -547,12 +547,13 @@ end
function FrontLightWidget:onCloseWidget() function FrontLightWidget:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.light_frame.dimen return "flashpartial", self.light_frame.dimen
end) end)
return true return true
end end
function FrontLightWidget:onShow() function FrontLightWidget:onShow()
-- NOTE: Keep this one as UI, it'll get coalesced...
UIManager:setDirty(self, function() UIManager:setDirty(self, function()
return "ui", self.light_frame.dimen return "ui", self.light_frame.dimen
end) end)

@ -35,6 +35,12 @@ function HtmlBoxWidget:init()
}, },
} }
end end
if Mupdf.bgr == nil then
Mupdf.bgr = false
if Device:hasBGRFrameBuffer() then
Mupdf.bgr = true
end
end
end end
function HtmlBoxWidget:setContent(body, css, default_font_size) function HtmlBoxWidget:setContent(body, css, default_font_size)

@ -95,18 +95,16 @@ function IconButton:onTapIconButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
UIManager:scheduleIn(0.0, function() self.image.invert = true
self.image.invert = true UIManager:setDirty(self.show_parent, function()
UIManager:setDirty(self.show_parent, function() return "fast", self.dimen
return "ui", self[1].dimen
end)
end) end)
-- make sure button reacts before doing callback -- Make sure button reacts before doing callback
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self.image.invert = false self.image.invert = false
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen return "fast", self.dimen
end) end)
end) end)
end end
@ -126,12 +124,12 @@ end
function IconButton:onFocus() function IconButton:onFocus()
--quick and dirty, need better way to show focus --quick and dirty, need better way to show focus
self.image.invert=true self.image.invert = true
return true return true
end end
function IconButton:onUnfocus() function IconButton:onUnfocus()
self.image.invert=false self.image.invert = false
return true return true
end end

@ -602,7 +602,7 @@ function ImageViewer:onCloseWidget()
self._images_list.free() self._images_list.free()
end end
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self.main_frame.dimen return "flashui", self.main_frame.dimen
end) end)
return true return true
end end

@ -235,9 +235,9 @@ function KeyValueItem:onTap()
else else
self[1].invert = true self[1].invert = true
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen return "fast", self[1].dimen
end) end)
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self[1].invert = false self[1].invert = false
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()

@ -403,13 +403,14 @@ function MenuItem:onTapSelect(arg, ges)
coroutine.resume(co) coroutine.resume(co)
else else
self[1].invert = true self[1].invert = true
local refreshfunc = function() UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen return "fast", self[1].dimen
end end)
UIManager:setDirty(self.show_parent, refreshfunc) UIManager:tickAfterNext(function()
UIManager:scheduleIn(0.1, function()
self[1].invert = false self[1].invert = false
UIManager:setDirty(self.show_parent, refreshfunc) UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
logger.dbg("creating coroutine for menu select") logger.dbg("creating coroutine for menu select")
local co = coroutine.create(function() local co = coroutine.create(function()
self.menu:onMenuSelect(self.table, pos) self.menu:onMenuSelect(self.table, pos)
@ -426,13 +427,14 @@ function MenuItem:onHoldSelect(arg, ges)
self.menu:onMenuHold(self.table, pos) self.menu:onMenuHold(self.table, pos)
else else
self[1].invert = true self[1].invert = true
local refreshfunc = function() UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen return "fast", self[1].dimen
end end)
UIManager:setDirty(self.show_parent, refreshfunc) UIManager:tickAfterNext(function()
UIManager:scheduleIn(0.1, function()
self[1].invert = false self[1].invert = false
UIManager:setDirty(self.show_parent, refreshfunc) UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen
end)
self.menu:onMenuHold(self.table, pos) self.menu:onMenuHold(self.table, pos)
end) end)
end end
@ -807,6 +809,8 @@ function Menu:onCloseWidget()
-- For example, it's a dirty hack to use two menus(one this menu and one -- For example, it's a dirty hack to use two menus(one this menu and one
-- touch menu) in the filemanager in order to capture tap gesture to popup -- touch menu) in the filemanager in order to capture tap gesture to popup
-- the filemanager menu. -- the filemanager menu.
-- NOTE: For the same reason, don't make it flash,
-- because that'll trigger when we close the FM and open a book...
UIManager:setDirty(nil, "partial") UIManager:setDirty(nil, "partial")
end end

@ -73,7 +73,7 @@ end
function Notification:onCloseWidget() function Notification:onCloseWidget()
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()
return "partial", self[1][1].dimen return "ui", self[1][1].dimen
end) end)
return true return true
end end

@ -113,17 +113,15 @@ function RadioButton:onTapCheckButton()
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.callback() self.callback()
else else
UIManager:scheduleIn(0.0, function() self.invert = true
self.invert = true UIManager:setDirty(self.show_parent, function()
UIManager:setDirty(self.show_parent, function() return "fast", self.dimen
return "ui", self.dimen
end)
end) end)
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.callback() self.callback()
self.invert = false self.invert = false
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen return "fast", self.dimen
end) end)
end) end)
end end
@ -151,7 +149,7 @@ function RadioButton:check(callback)
self.checked = true self.checked = true
self:update() self:update()
UIManager:setDirty(self.parent, function() UIManager:setDirty(self.parent, function()
return "ui", self.dimen return "fast", self.dimen
end) end)
end end
@ -160,7 +158,7 @@ function RadioButton:unCheck()
self.checked = false self.checked = false
self:update() self:update()
UIManager:setDirty(self.parent, function() UIManager:setDirty(self.parent, function()
return "ui", self.dimen return "fast", self.dimen
end) end)
end end

@ -246,8 +246,8 @@ function TextViewer:onSwipe(arg, ges)
self.scroll_text_w:scrollText(-1) self.scroll_text_w:scrollText(-1)
return true return true
else else
-- trigger full refresh -- trigger a flashing text refresh
UIManager:setDirty(nil, "full") UIManager:setDirty(nil, "flashui", self.frame.dimen)
-- a long diagonal swipe may also be used for taking a screenshot, -- a long diagonal swipe may also be used for taking a screenshot,
-- so let it propagate -- so let it propagate
return false return false

@ -105,7 +105,7 @@ function TouchMenuItem:init()
self._underline_container = UnderlineContainer:new{ self._underline_container = UnderlineContainer:new{
vertical_align = "center", vertical_align = "center",
dimen =self.dimen, dimen = self.dimen,
self.item_frame self.item_frame
} }
@ -137,15 +137,20 @@ function TouchMenuItem:onTapSelect(arg, ges)
else else
self.item_frame.invert = true self.item_frame.invert = true
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen return "fast", self.dimen
end) end)
-- yield to main UI loop to invert item -- yield to main UI loop to invert item
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.menu:onMenuSelect(self.item) self.menu:onMenuSelect(self.item)
self.item_frame.invert = false self.item_frame.invert = false
--[[
-- NOTE: We can optimize that repaint away, every entry in our menu will make at least the menu repaint just after anyways ;).
-- Plus, leaving that unhighlight as "fast" can lead to weird side-effects, depending on devices.
-- If it turns out this need to go back in, consider switching it to "ui".
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen return "fast", self.dimen
end) end)
--]]
end) end)
end end
return true return true
@ -161,13 +166,11 @@ function TouchMenuItem:onHoldSelect(arg, ges)
if G_reader_settings:isFalse("flash_ui") then if G_reader_settings:isFalse("flash_ui") then
self.menu:onMenuHold(self.item) self.menu:onMenuHold(self.item)
else else
UIManager:scheduleIn(0.0, function() self.item_frame.invert = true
self.item_frame.invert = true UIManager:setDirty(self.show_parent, function()
UIManager:setDirty(self.show_parent, function() return "fast", self.dimen
return "ui", self.dimen
end)
end) end)
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.menu:onMenuHold(self.item) self.menu:onMenuHold(self.item)
end) end)
UIManager:scheduleIn(0.5, function() UIManager:scheduleIn(0.5, function()
@ -348,7 +351,7 @@ TouchMenu widget for hierarchical menus
--]] --]]
local TouchMenu = FocusManager:new{ local TouchMenu = FocusManager:new{
tab_item_table = {}, tab_item_table = {},
-- for returnning in multi-level menus -- for returning in multi-level menus
item_table_stack = nil, item_table_stack = nil,
item_table = nil, item_table = nil,
item_height = Size.item.height_large, item_height = Size.item.height_large,
@ -363,6 +366,7 @@ local TouchMenu = FocusManager:new{
show_parent = nil, show_parent = nil,
cur_tab = -1, cur_tab = -1,
close_callback = nil, close_callback = nil,
is_fresh = true,
} }
function TouchMenu:init() function TouchMenu:init()
@ -440,7 +444,7 @@ function TouchMenu:init()
self.page_info_text, self.page_info_text,
self.page_info_right_chev self.page_info_right_chev
} }
--group for device info -- group for device info
self.time_info = TextWidget:new{ self.time_info = TextWidget:new{
text = "", text = "",
face = self.fface, face = self.fface,
@ -508,7 +512,8 @@ function TouchMenu:init()
end end
function TouchMenu:onCloseWidget() function TouchMenu:onCloseWidget()
UIManager:setDirty(nil, "partial", self.dimen) -- NOTE: We pass a nil region to ensure a full-screen flash to avoid ghosting
UIManager:setDirty(nil, "flashui", nil)
end end
function TouchMenu:_recalculatePageLayout() function TouchMenu:_recalculatePageLayout()
@ -543,7 +548,7 @@ function TouchMenu:updateItems()
self.item_group:clear() self.item_group:clear()
self.layout = {} self.layout = {}
table.insert(self.item_group, self.bar) table.insert(self.item_group, self.bar)
table.insert(self.layout, self.bar.icon_widgets) --for the focusmanager table.insert(self.layout, self.bar.icon_widgets) -- for the focusmanager
for c = 1, self.perpage do for c = 1, self.perpage do
-- calculate index in item_table -- calculate index in item_table
@ -561,7 +566,7 @@ function TouchMenu:updateItems()
} }
table.insert(self.item_group, item_tmp) table.insert(self.item_group, item_tmp)
if item_tmp:isEnabled() then if item_tmp:isEnabled() then
table.insert(self.layout, {[self.cur_tab] = item_tmp}) --for the focusmanager table.insert(self.layout, {[self.cur_tab] = item_tmp}) -- for the focusmanager
end end
if item.separator and c ~= self.perpage then if item.separator and c ~= self.perpage then
-- insert split line -- insert split line
@ -590,13 +595,22 @@ function TouchMenu:updateItems()
-- recalculate dimen based on new layout -- recalculate dimen based on new layout
self.dimen.w = self.width self.dimen.w = self.width
self.dimen.h = self.item_group:getSize().h + self.bordersize*2 + self.padding*2 self.dimen.h = self.item_group:getSize().h + self.bordersize*2 + self.padding*2
self.selected = { x = self.cur_tab, y = 1 } --reset the position of the focusmanager self.selected = { x = self.cur_tab, y = 1 } -- reset the position of the focusmanager
-- NOTE: We use a slightly ugly hack to detect a brand new menu vs. a tab switch,
-- in order to optionally flash on initial menu popup...
UIManager:setDirty("all", function() UIManager:setDirty("all", function()
local refresh_dimen = local refresh_dimen =
old_dimen and old_dimen:combine(self.dimen) old_dimen and old_dimen:combine(self.dimen)
or self.dimen or self.dimen
return "ui", refresh_dimen local refresh_type = "ui"
if self.is_fresh then
refresh_type = "flashui"
-- Drop the region, too, to make it full-screen? May help when starting from a "small" menu.
--refresh_dimen = nil
self.is_fresh = false
end
return refresh_type, refresh_dimen
end) end)
end end
@ -690,7 +704,7 @@ function TouchMenu:onMenuSelect(item)
if callback then if callback then
-- put stuff in scheduler so we can see -- put stuff in scheduler so we can see
-- the effect of inverted menu item -- the effect of inverted menu item
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
callback(self) callback(self)
if refresh then if refresh then
self:updateItems() self:updateItems()
@ -726,7 +740,7 @@ function TouchMenu:onMenuHold(item)
callback = item.hold_callback_func() callback = item.hold_callback_func()
end end
if callback then if callback then
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
if item.hold_may_update_menu then if item.hold_may_update_menu then
callback(function() self:updateItems() end) callback(function() self:updateItems() end)
else else

@ -108,6 +108,8 @@ function VirtualKey:init()
end end
function VirtualKey:update_keyboard() function VirtualKey:update_keyboard()
-- NOTE: We could arguably use "fast" when inverted & "ui" when not, but it doesn't change much,
-- and doesn't help with the graphics quirks of repeated "fast" updates on some devices.
UIManager:setDirty(self.keyboard, function() UIManager:setDirty(self.keyboard, function()
logger.dbg("update key region", self[1].dimen) logger.dbg("update key region", self[1].dimen)
return "fast", self[1].dimen return "fast", self[1].dimen
@ -129,7 +131,7 @@ function VirtualKey:onTapSelect()
if self.callback then if self.callback then
self.callback() self.callback()
end end
UIManager:scheduleIn(0.1, function() self:invert(false) end) UIManager:tickAfterNext(function() self:invert(false) end)
else else
if self.callback then if self.callback then
self.callback() self.callback()
@ -145,7 +147,7 @@ function VirtualKey:onHoldSelect()
if self.hold_callback then if self.hold_callback then
self.hold_callback() self.hold_callback()
end end
UIManager:scheduleIn(0.1, function() self:invert(false) end) UIManager:tickAfterNext(function() self:invert(false) end)
else else
if self.hold_callback then if self.hold_callback then
self.hold_callback() self.hold_callback()
@ -219,20 +221,23 @@ function VirtualKeyboard:onPressKey()
return true return true
end end
function VirtualKeyboard:_refresh() function VirtualKeyboard:_refresh(want_flash)
-- TODO: Ideally, ui onShow & partial onClose local refresh_type = "partial"
if want_flash then
refresh_type = "flashui"
end
UIManager:setDirty(self, function() UIManager:setDirty(self, function()
return "ui", self[1][1].dimen return refresh_type, self[1][1].dimen
end) end)
end end
function VirtualKeyboard:onShow() function VirtualKeyboard:onShow()
self:_refresh() self:_refresh(true)
return true return true
end end
function VirtualKeyboard:onCloseWidget() function VirtualKeyboard:onCloseWidget()
self:_refresh() self:_refresh(false)
return true return true
end end
@ -331,7 +336,7 @@ function VirtualKeyboard:setLayout(key)
if self.utf8mode then self.umlautmode = false end if self.utf8mode then self.umlautmode = false end
end end
self:initLayout() self:initLayout()
self:_refresh() self:_refresh(true)
end end
function VirtualKeyboard:addChar(key) function VirtualKeyboard:addChar(key)

@ -3,6 +3,7 @@ A.dl.library_path = A.dl.library_path .. ":" .. A.dir .. "/libs"
A.log_name = 'KOReader' A.log_name = 'KOReader'
local ffi = require("ffi") local ffi = require("ffi")
local C = ffi.C
ffi.cdef[[ ffi.cdef[[
char *getenv(const char *name); char *getenv(const char *name);
int putenv(const char *envvar); int putenv(const char *envvar);
@ -56,7 +57,7 @@ A.execute("chmod", "755", "./tar")
A.execute("chmod", "755", "./zsync") A.execute("chmod", "755", "./zsync")
-- set TESSDATA_PREFIX env var -- set TESSDATA_PREFIX env var
ffi.C.putenv("TESSDATA_PREFIX=/sdcard/koreader/data") C.putenv("TESSDATA_PREFIX=/sdcard/koreader/data")
-- create fake command-line arguments -- create fake command-line arguments
arg = {"-d", file or "/sdcard"} arg = {"-d", file or "/sdcard"}

@ -126,8 +126,8 @@ export TESSDATA_PREFIX="data"
# export dict directory # export dict directory
export STARDICT_DATA_DIR="data/dict" export STARDICT_DATA_DIR="data/dict"
# export external font directory # export external font directories (In order: stock, legacy custom, stock extra, font hack)
export EXT_FONT_DIR="/mnt/us/fonts" export EXT_FONT_DIR="/usr/java/lib/fonts;/mnt/us/fonts;/var/local/font/mnt;/mnt/us/linkfonts/fonts"
# Only setup IPTables on evices where it makes sense to (FW 5.x & K4) # Only setup IPTables on evices where it makes sense to (FW 5.x & K4)
if [ "${INIT_TYPE}" = "upstart" ] || [ "$(uname -r)" = "2.6.31-rt11-lab126" ]; then if [ "${INIT_TYPE}" = "upstart" ] || [ "$(uname -r)" = "2.6.31-rt11-lab126" ]; then
@ -139,39 +139,6 @@ if [ "${INIT_TYPE}" = "upstart" ] || [ "$(uname -r)" = "2.6.31-rt11-lab126" ]; t
iptables -A INPUT -i wlan0 -p udp --dport 8134 -j ACCEPT iptables -A INPUT -i wlan0 -p udp --dport 8134 -j ACCEPT
fi fi
# bind-mount system fonts
if ! grep ${KOREADER_DIR}/fonts/host /proc/mounts >/dev/null 2>&1; then
logmsg "Mounting system fonts . . ."
mount -o bind /usr/java/lib/fonts ${KOREADER_DIR}/fonts/host
fi
# bind-mount altfonts
if [ -d /mnt/us/fonts ]; then
mkdir -p ${KOREADER_DIR}/fonts/altfonts
if ! grep ${KOREADER_DIR}/fonts/altfonts /proc/mounts >/dev/null 2>&1; then
logmsg "Mounting altfonts . . ."
mount -o bind /mnt/us/fonts ${KOREADER_DIR}/fonts/altfonts
fi
fi
# bind-mount csp fonts
if [ -d /var/local/font/mnt ]; then
mkdir -p ${KOREADER_DIR}/fonts/cspfonts
if ! grep ${KOREADER_DIR}/fonts/cspfonts /proc/mounts >/dev/null 2>&1; then
logmsg "Mounting cspfonts . . ."
mount -o bind /var/local/font/mnt ${KOREADER_DIR}/fonts/cspfonts
fi
fi
# bind-mount linkfonts
if [ -d /mnt/us/linkfonts/fonts ]; then
mkdir -p ${KOREADER_DIR}/fonts/linkfonts
if ! grep ${KOREADER_DIR}/fonts/linkfonts /proc/mounts >/dev/null 2>&1; then
logmsg "Mounting linkfonts . . ."
mount -o bind /mnt/us/linkfonts/fonts ${KOREADER_DIR}/fonts/linkfonts
fi
fi
# check if we need to disable the system passcode, because it messes with us in fun and interesting (and, more to the point, intractable) ways... # check if we need to disable the system passcode, because it messes with us in fun and interesting (and, more to the point, intractable) ways...
# NOTE: The most egregious one being that it inhibits the outOfScreenSaver event on wakeup until the passcode is validated, which we can't do, since we capture all input... # NOTE: The most egregious one being that it inhibits the outOfScreenSaver event on wakeup until the passcode is validated, which we can't do, since we capture all input...
if [ -f "/var/local/system/userpasswdenabled" ]; then if [ -f "/var/local/system/userpasswdenabled" ]; then
@ -276,30 +243,6 @@ if pidof reader.lua >/dev/null 2>&1; then
killall -TERM reader.lua killall -TERM reader.lua
fi fi
# unmount system fonts
if grep ${KOREADER_DIR}/fonts/host /proc/mounts >/dev/null 2>&1; then
logmsg "Unmounting system fonts . . ."
umount ${KOREADER_DIR}/fonts/host
fi
# unmount altfonts
if grep ${KOREADER_DIR}/fonts/altfonts /proc/mounts >/dev/null 2>&1; then
logmsg "Unmounting altfonts . . ."
umount ${KOREADER_DIR}/fonts/altfonts
fi
# unmount cspfonts
if grep ${KOREADER_DIR}/fonts/cspfonts /proc/mounts >/dev/null 2>&1; then
logmsg "Unmounting cspfonts . . ."
umount ${KOREADER_DIR}/fonts/cspfonts
fi
# unmount linkfonts
if grep ${KOREADER_DIR}/fonts/linkfonts /proc/mounts >/dev/null 2>&1; then
logmsg "Unmounting linkfonts . . ."
umount ${KOREADER_DIR}/fonts/linkfonts
fi
# Resume volumd, if need be # Resume volumd, if need be
if [ "${VOLUMD_STOPPED}" = "yes" ]; then if [ "${VOLUMD_STOPPED}" = "yes" ]; then
logmsg "Resuming volumd . . ." logmsg "Resuming volumd . . ."

@ -91,7 +91,7 @@ function CoverMenu:updateItems(select_number)
local refresh_dimen = local refresh_dimen =
old_dimen and old_dimen:combine(self.dimen) old_dimen and old_dimen:combine(self.dimen)
or self.dimen or self.dimen
return "ui", refresh_dimen return "partial", refresh_dimen
end) end)
-- As additionally done in FileChooser:updateItems() -- As additionally done in FileChooser:updateItems()
@ -205,6 +205,8 @@ function CoverMenu:updateItems(select_number)
-- Close original ButtonDialogTitle (it has not yet been painted -- Close original ButtonDialogTitle (it has not yet been painted
-- on screen, so we won't see it) -- on screen, so we won't see it)
UIManager:close(self.file_dialog) UIManager:close(self.file_dialog)
-- And clear the rendering stack to avoid inheriting its dirty/refresh queue
UIManager:clearRenderStack()
-- Replace Book information callback to use directly our bookinfo -- Replace Book information callback to use directly our bookinfo
orig_buttons[4][3].callback = function() orig_buttons[4][3].callback = function()
@ -320,6 +322,7 @@ function CoverMenu:onHistoryMenuHold(item)
-- Close original ButtonDialog (it has not yet been painted -- Close original ButtonDialog (it has not yet been painted
-- on screen, so we won't see it) -- on screen, so we won't see it)
UIManager:close(self.histfile_dialog) UIManager:close(self.histfile_dialog)
UIManager:clearRenderStack()
-- Replace Book information callback to use directly our bookinfo -- Replace Book information callback to use directly our bookinfo
orig_buttons[2][2].callback = function() orig_buttons[2][2].callback = function()
@ -471,6 +474,7 @@ function CoverMenu:tapPlus()
-- Close original ButtonDialogTitle (it has not yet been painted -- Close original ButtonDialogTitle (it has not yet been painted
-- on screen, so we won't see it) -- on screen, so we won't see it)
UIManager:close(self.file_dialog) UIManager:close(self.file_dialog)
UIManager:clearRenderStack()
-- Add a new button to original buttons set -- Add a new button to original buttons set
table.insert(orig_buttons, {}) -- separator table.insert(orig_buttons, {}) -- separator

@ -174,9 +174,9 @@ function DoubleKeyValueItem:onTap()
else else
self[1].invert = true self[1].invert = true
UIManager:setDirty(self.show_parent, function() UIManager:setDirty(self.show_parent, function()
return "ui", self[1].dimen return "fast", self[1].dimen
end) end)
UIManager:scheduleIn(0.1, function() UIManager:tickAfterNext(function()
self.callback() self.callback()
UIManager:close(info) UIManager:close(info)
self[1].invert = false self[1].invert = false

@ -10,6 +10,7 @@ local NetworkMgr = require("ui/network/manager")
local WidgetContainer = require("ui/widget/container/widgetcontainer") local WidgetContainer = require("ui/widget/container/widgetcontainer")
local dateparser = require("lib.dateparser") local dateparser = require("lib.dateparser")
local ffi = require("ffi") local ffi = require("ffi")
local C = ffi.C
local logger = require("logger") local logger = require("logger")
local util = require("util") local util = require("util")
local _ = require("gettext") local _ = require("gettext")
@ -355,7 +356,7 @@ function NewsDownloader:removeNewsButKeepFeedConfig()
local entry_path = news_download_dir_path .. "/" .. entry local entry_path = news_download_dir_path .. "/" .. entry
local entry_mode = lfs.attributes(entry_path, "mode") local entry_mode = lfs.attributes(entry_path, "mode")
if entry_mode == "file" then if entry_mode == "file" then
ffi.C.remove(entry_path) C.remove(entry_path)
elseif entry_mode == "directory" then elseif entry_mode == "directory" then
FFIUtil.purgeDir(entry_path) FFIUtil.purgeDir(entry_path)
end end

@ -6,6 +6,7 @@ local DEBUG = require("dbg")
local _ = require("gettext") local _ = require("gettext")
local ffi = require("ffi") local ffi = require("ffi")
local C = ffi.C
ffi.cdef[[ ffi.cdef[[
int remove(const char *); int remove(const char *);
int rmdir(const char *); int rmdir(const char *);
@ -131,13 +132,13 @@ local function clearDirectory(dir, rmdir)
local path = dir.."/"..f local path = dir.."/"..f
local mode = lfs.attributes(path, "mode") local mode = lfs.attributes(path, "mode")
if mode == "file" then if mode == "file" then
ffi.C.remove(path) C.remove(path)
elseif mode == "directory" and f ~= "." and f ~= ".." then elseif mode == "directory" and f ~= "." and f ~= ".." then
clearDirectory(path, true) clearDirectory(path, true)
end end
end end
if rmdir then if rmdir then
ffi.C.rmdir(dir) C.rmdir(dir)
end end
end end

Loading…
Cancel
Save