2
0
mirror of https://github.com/koreader/koreader synced 2024-11-04 12:00:25 +00:00
koreader/frontend/device/sdl/device.lua
NiLuJe fadee1f5dc
Clarify our OOP semantics across the codebase (#9586)
Basically:

* Use `extend` for class definitions
* Use `new` for object instantiations

That includes some minor code cleanups along the way:

* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
  * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 02:14:48 +02:00

422 lines
14 KiB
Lua

local Event = require("ui/event")
local Generic = require("device/generic/device")
local SDL = require("ffi/SDL2_0")
local ffi = require("ffi")
local logger = require("logger")
local time = require("ui/time")
-- SDL computes WM_CLASS on X11/Wayland based on process's binary name.
-- Some desktop environments rely on WM_CLASS to name the app and/or to assign the proper icon.
if jit.os == "Linux" or jit.os == "BSD" or jit.os == "POSIX" then
if not os.getenv("SDL_VIDEO_WAYLAND_WMCLASS") then ffi.C.setenv("SDL_VIDEO_WAYLAND_WMCLASS", "KOReader", 1) end
if not os.getenv("SDL_VIDEO_X11_WMCLASS") then ffi.C.setenv("SDL_VIDEO_X11_WMCLASS", "KOReader", 1) end
end
local function yes() return true end
local function no() return false end
local function notOSX() return jit.os ~= "OSX" end
local function isUrl(s)
return type(s) == "string" and s:match("*?://")
end
local function isCommand(s)
return os.execute("which "..s.." >/dev/null 2>&1") == 0
end
local function runCommand(command)
local env = jit.os ~= "OSX" and 'env -u LD_LIBRARY_PATH ' or ""
return os.execute(env..command) == 0
end
local function getDesktopDicts()
local t = {
{ "Goldendict", "Goldendict", false, "goldendict" },
}
-- apple dict is always present in osx
if jit.os == "OSX" then
table.insert(t, 1, { "Apple", "AppleDict", false, "dict://" })
end
return t
end
local function getLinkOpener()
if jit.os == "Linux" and isCommand("xdg-open") then
return true, "xdg-open"
elseif jit.os == "OSX" and isCommand("open") then
return true, "open"
end
return false
end
-- thirdparty app support
local external = require("device/thirdparty"):new{
dicts = getDesktopDicts(),
check = function(self, app)
if (isUrl(app) and getLinkOpener()) or isCommand(app) then
return true
end
return false
end,
}
local Device = Generic:extend{
model = "SDL",
isSDL = yes,
home_dir = os.getenv("XDG_DOCUMENTS_DIR") or os.getenv("HOME"),
hasBattery = SDL.getPowerInfo,
hasKeyboard = yes,
hasKeys = yes,
hasDPad = yes,
hasWifiToggle = no,
isTouchDevice = yes,
isDefaultFullscreen = no,
needsScreenRefreshAfterResume = no,
hasColorScreen = yes,
hasEinkScreen = no,
hasSystemFonts = yes,
canSuspend = no,
canStandby = no,
startTextInput = SDL.startTextInput,
stopTextInput = SDL.stopTextInput,
canOpenLink = getLinkOpener,
openLink = function(self, link)
local enabled, tool = getLinkOpener()
if not enabled or not tool or not link or type(link) ~= "string" then return end
return runCommand(tool .. " '" .. link .. "'")
end,
canExternalDictLookup = yes,
getExternalDictLookupList = function() return external.dicts end,
doExternalDictLookup = function(self, text, method, callback)
external.when_back_callback = callback
local ok, app = external:checkMethod("dict", method)
if app then
if isUrl(app) and getLinkOpener() then
ok = self:openLink(app..text)
elseif isCommand(app) then
ok = runCommand(app .. " " .. text .. " &")
end
end
if ok and external.when_back_callback then
external.when_back_callback()
external.when_back_callback = nil
end
end,
window = G_reader_settings:readSetting("sdl_window", {}),
}
local AppImage = Device:extend{
model = "AppImage",
hasMultitouch = no,
hasOTAUpdates = yes,
isDesktop = yes,
}
local Desktop = Device:extend{
model = SDL.getPlatform(),
isDesktop = yes,
canRestart = notOSX,
hasExitOptions = notOSX,
}
local Emulator = Device:extend{
model = "Emulator",
isEmulator = yes,
hasBattery = yes,
hasEinkScreen = yes,
hasFrontlight = yes,
hasNaturalLight = yes,
hasNaturalLightApi = yes,
hasWifiToggle = yes,
hasWifiManager = yes,
-- Not really, Device:reboot & Device:powerOff are not implemented, so we just exit ;).
canPowerOff = yes,
canReboot = yes,
-- NOTE: Via simulateSuspend
canSuspend = yes,
canStandby = no,
}
local UbuntuTouch = Device:extend{
model = "UbuntuTouch",
hasFrontlight = yes,
isDefaultFullscreen = yes,
}
function Device:init()
-- allows to set a viewport via environment variable
-- syntax is Lua table syntax, e.g. EMULATE_READER_VIEWPORT="{x=10,w=550,y=5,h=790}"
local viewport = os.getenv("EMULATE_READER_VIEWPORT")
if viewport then
self.viewport = require("ui/geometry"):new(loadstring("return " .. viewport)())
end
local touchless = os.getenv("DISABLE_TOUCH") == "1"
if touchless then
self.isTouchDevice = no
end
local portrait = os.getenv("EMULATE_READER_FORCE_PORTRAIT")
if portrait then
self.isAlwaysPortrait = yes
end
self.hasClipboard = yes
self.screen = require("ffi/framebuffer_SDL2_0"):new{
device = self,
debug = logger.dbg,
w = self.window.width,
h = self.window.height,
x = self.window.left,
y = self.window.top,
is_always_portrait = self.isAlwaysPortrait(),
}
self.powerd = require("device/sdl/powerd"):new{device = self}
local ok, re = pcall(self.screen.setWindowIcon, self.screen, "resources/koreader.png")
if not ok then logger.warn(re) end
local input = require("ffi/input")
self.input = require("device/input"):new{
device = self,
event_map = require("device/sdl/event_map_sdl2"),
handleSdlEv = function(device_input, ev)
local Geom = require("ui/geometry")
local UIManager = require("ui/uimanager")
-- SDL events can remain cdata but are almost completely transparent
local SDL_TEXTINPUT = 771
local SDL_MOUSEWHEEL = 1027
local SDL_MULTIGESTURE = 2050
local SDL_DROPFILE = 4096
local SDL_WINDOWEVENT_MOVED = 4
local SDL_WINDOWEVENT_RESIZED = 5
if ev.code == SDL_MOUSEWHEEL then
local scrolled_x = ev.value.x
local scrolled_y = ev.value.y
local up = 1
local down = -1
local pos = Geom:new{
x = 0,
y = 0,
w = 0, h = 0,
}
local fake_ges = {
ges = "pan",
distance = 200,
relative = {
x = 50*scrolled_x,
y = 100*scrolled_y,
},
pos = pos,
time = time.timeval(ev.time),
mousewheel_direction = scrolled_y,
}
local fake_ges_release = {
ges = "pan_release",
distance = fake_ges.distance,
relative = fake_ges.relative,
pos = pos,
time = time.timeval(ev.time),
from_mousewheel = true,
}
local fake_pan_ev = Event:new("Pan", nil, fake_ges)
local fake_release_ev = Event:new("Gesture", fake_ges_release)
if scrolled_y == down then
fake_ges.direction = "north"
UIManager:broadcastEvent(fake_pan_ev)
UIManager:broadcastEvent(fake_release_ev)
elseif scrolled_y == up then
fake_ges.direction = "south"
UIManager:broadcastEvent(fake_pan_ev)
UIManager:broadcastEvent(fake_release_ev)
end
elseif ev.code == SDL_MULTIGESTURE then
-- no-op for now
do end -- luacheck: ignore 541
elseif ev.code == SDL_DROPFILE then
local dropped_file_path = ev.value
if dropped_file_path and dropped_file_path ~= "" then
local ReaderUI = require("apps/reader/readerui")
ReaderUI:doShowReader(dropped_file_path)
end
elseif ev.code == SDL_WINDOWEVENT_RESIZED then
device_input.device.screen.screen_size.w = ev.value.data1
device_input.device.screen.screen_size.h = ev.value.data2
device_input.device.screen.resize(device_input.device.screen, ev.value.data1, ev.value.data2)
self.window.width = ev.value.data1
self.window.height = ev.value.data2
local new_size = device_input.device.screen:getSize()
logger.dbg("Resizing screen to", new_size)
-- try to catch as many flies as we can
-- this means we can't just return one ScreenResize or SetDimensons event
UIManager:broadcastEvent(Event:new("SetDimensions", new_size))
UIManager:broadcastEvent(Event:new("ScreenResize", new_size))
--- @todo Toggle this elsewhere based on ScreenResize?
-- this triggers paged media like PDF and DjVu to redraw
-- CreDocument doesn't need it
UIManager:broadcastEvent(Event:new("RedrawCurrentPage"))
local FileManager = require("apps/filemanager/filemanager")
if FileManager.instance then
FileManager.instance:reinit(FileManager.instance.path,
FileManager.instance.focused_file)
end
elseif ev.code == SDL_WINDOWEVENT_MOVED then
self.window.left = ev.value.data1
self.window.top = ev.value.data2
elseif ev.code == SDL_TEXTINPUT then
UIManager:sendEvent(Event:new("TextInput", tostring(ev.value)))
end
end,
hasClipboardText = function()
return input.hasClipboardText()
end,
getClipboardText = function()
return input.getClipboardText()
end,
setClipboardText = function(text)
return input.setClipboardText(text)
end,
gameControllerRumble = function(left_intensity, right_intensity, duration)
return input.gameControllerRumble(left_intensity, right_intensity, duration)
end,
file_chooser = input.file_chooser,
}
self.keyboard_layout = require("device/sdl/keyboard_layout")
if self.input.gameControllerRumble(0, 0, 0) then
self.isHapticFeedbackEnabled = yes
self.performHapticFeedback = function(type)
self.input.gameControllerRumble()
end
end
if portrait then
self.input:registerEventAdjustHook(self.input.adjustTouchSwitchXY)
self.input:registerEventAdjustHook(
self.input.adjustTouchMirrorX,
(self.screen:getScreenWidth() - 1)
)
end
Generic.init(self)
end
function Device:setDateTime(year, month, day, hour, min, sec)
if hour == nil or min == nil then return true end
local command
if year and month and day then
command = string.format("date -s '%d-%d-%d %d:%d:%d'", year, month, day, hour, min, sec)
else
command = string.format("date -s '%d:%d'",hour, min)
end
if os.execute(command) == 0 then
os.execute('hwclock -u -w')
return true
else
return false
end
end
function Device:isAlwaysFullscreen()
-- return true on embedded devices, which should default to fullscreen
return self:isDefaultFullscreen()
end
function Device:toggleFullscreen()
local current_mode = self.fullscreen or self:isDefaultFullscreen()
local new_mode = not current_mode
local ok, err = SDL.setWindowFullscreen(new_mode)
if not ok then
logger.warn("Unable to toggle fullscreen mode to", new_mode, "\n", err)
else
self.fullscreen = new_mode
end
end
function Device:setEventHandlers(UIManager)
if not self:canSuspend() then
-- If we can't suspend, we have no business even trying to, as we may not have overloaded `Device:simulateResume`,
-- and since the empty Generic prototype doesn't flip `Device.screen_saver_mode`, we'd be stuck if we tried...
-- Instead, rely on the Generic Suspend/Resume handlers, which are sane ;).
return
end
UIManager.event_handlers.Suspend = function()
self:_beforeSuspend()
self:simulateSuspend()
end
UIManager.event_handlers.Resume = function()
self:simulateResume()
self:_afterResume()
end
UIManager.event_handlers.PowerRelease = function()
-- Resume if we were suspended
if self.screen_saver_mode then
UIManager.event_handlers.Resume()
else
UIManager.event_handlers.Suspend()
end
end
end
function Emulator:supportsScreensaver() return true end
function Emulator:simulateSuspend()
local Screensaver = require("ui/screensaver")
Screensaver:setup()
Screensaver:show()
self.screen_saver_mode = true
end
function Emulator:simulateResume()
local Screensaver = require("ui/screensaver")
Screensaver:close()
self.screen_saver_mode = false
end
-- fake network manager for the emulator
function Emulator:initNetworkManager(NetworkMgr)
local UIManager = require("ui/uimanager")
local connectionChangedEvent = function()
if G_reader_settings:nilOrTrue("emulator_fake_wifi_connected") then
UIManager:broadcastEvent(Event:new("NetworkConnected"))
else
UIManager:broadcastEvent(Event:new("NetworkDisconnected"))
end
end
function NetworkMgr:turnOffWifi(complete_callback)
G_reader_settings:flipNilOrTrue("emulator_fake_wifi_connected")
UIManager:scheduleIn(2, connectionChangedEvent)
end
function NetworkMgr:turnOnWifi(complete_callback)
G_reader_settings:flipNilOrTrue("emulator_fake_wifi_connected")
UIManager:scheduleIn(2, connectionChangedEvent)
end
function NetworkMgr:isWifiOn()
return G_reader_settings:nilOrTrue("emulator_fake_wifi_connected")
end
end
io.write("Starting SDL in " .. SDL.getBasePath() .. "\n")
-------------- device probe ------------
if os.getenv("APPIMAGE") then
return AppImage
elseif os.getenv("KO_MULTIUSER") then
return Desktop
elseif os.getenv("UBUNTU_APPLICATION_ISOLATION") then
return UbuntuTouch
else
return Emulator
end