2
0
mirror of https://github.com/koreader/koreader synced 2024-11-10 01:10:34 +00:00
koreader/frontend/device/sdl/device.lua
NiLuJe 761cf18222
SDL: Return true in NetworkMgr:isConnected on !hasWifiToggle platforms, (#12217)
so as not to break the beforeWifiction shenanigans...

Which is most of them, only the Emulator subclass sets it, for...
reasons.

(That Emulator quirk is also why we can't simply scrap the whole thing
to use NetworkMgr's default imps).

Fix #12203 (thanks to @benoit-pierre for spotting that one).

This was clearly an oversight on my part when working on #10669
2024-07-26 02:12:53 +02:00

470 lines
16 KiB
Lua

local Event = require("ui/event")
local Geom = require("ui/geometry")
local Generic = require("device/generic/device")
local UIManager
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("command -v "..s.." >/dev/null") == 0
end
local function runCommand(command)
return os.execute(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,
hasSymKey = os.getenv("DISABLE_TOUCH") == "1" and yes or no,
hasDPad = yes,
hasWifiToggle = no,
hasSeamlessWifiToggle = 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", {}),
}
function Device:otaModel()
if self.ota_model then
return self.ota_model, "link"
end
end
local AppImage = Device:extend{
model = "AppImage",
ota_model = "appimage",
hasOTAUpdates = yes,
isDesktop = yes,
}
local Desktop = Device:extend{
model = SDL.getPlatform(),
isDesktop = yes,
canRestart = notOSX,
hasExitOptions = notOSX,
}
local Flatpak = Device:extend{
model = "Flatpak",
isDesktop = yes,
canExternalDictLookup = no,
}
local Emulator = Device:extend{
model = "Emulator",
isEmulator = yes,
hasBattery = yes,
hasEinkScreen = yes,
hasFrontlight = yes,
hasNaturalLight = yes,
hasNaturalLightApi = yes,
hasWifiToggle = 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(),
}
-- Pickup the updated window sizes if they were enforced in S.open (we'll get the coordinates via the inital SDL_WINDOWEVENT_MOVED)...
self.window.width = self.screen.w
self.window.height = self.screen.h
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)
-- 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:sendEvent(fake_pan_ev)
UIManager:sendEvent(fake_release_ev)
elseif scrolled_y == up then
fake_ges.direction = "south"
UIManager:sendEvent(fake_pan_ev)
UIManager:sendEvent(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.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()
device_input.device.screen.screen_size.w = new_size.w
device_input.device.screen.screen_size.h = new_size.h
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
-- make sure dialogs are displayed
UIManager:setDirty("all", "ui")
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.adjustTouchSwitchAxesAndMirrorX,
(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:UIManagerReady(uimgr)
UIManager = uimgr
end
function Device:setEventHandlers(uimgr)
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`.
-- Instead, rely on the Generic Suspend/Resume handlers.
return
end
UIManager.event_handlers.Suspend = function()
self:simulateSuspend()
end
UIManager.event_handlers.Resume = function()
self:simulateResume()
end
UIManager.event_handlers.PowerRelease = function()
-- Resume if we were suspended
if self.screen_saver_mode then
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end
end
end
function Device:initNetworkManager(NetworkMgr)
function NetworkMgr:isWifiOn() return true end
function NetworkMgr:isConnected()
if not Device:hasWifiToggle() then
-- NOTE: This is necessary so as not to confuse NetworkMghr's beforeWifiAction framework.
-- c.f., the default implementation in `NetworkMgr` itself for more details.
return true
end
-- Pull the default gateway first, so we don't even try to ping anything if there isn't one...
local default_gw = Device:getDefaultRoute()
if not default_gw then
return false
end
return 0 == os.execute("ping -c1 -w2 " .. default_gw .. " > /dev/null")
end
end
function Emulator:supportsScreensaver() return true end
function Emulator:simulateSuspend()
local Screensaver = require("ui/screensaver")
Screensaver:setup()
Screensaver:show()
self.powerd:beforeSuspend()
end
function Emulator:simulateResume()
local Screensaver = require("ui/screensaver")
Screensaver:close()
self.powerd:afterResume()
end
-- fake network manager for the emulator
function Emulator:initNetworkManager(NetworkMgr)
local connectionChangedEvent = function(complete_callback)
if G_reader_settings:nilOrTrue("emulator_fake_wifi_connected") then
UIManager:broadcastEvent(Event:new("NetworkConnected"))
else
UIManager:broadcastEvent(Event:new("NetworkDisconnected"))
end
if complete_callback then
complete_callback()
end
end
function NetworkMgr:turnOffWifi(complete_callback)
G_reader_settings:flipNilOrTrue("emulator_fake_wifi_connected")
UIManager:scheduleIn(2, connectionChangedEvent, complete_callback)
end
function NetworkMgr:turnOnWifi(complete_callback)
G_reader_settings:flipNilOrTrue("emulator_fake_wifi_connected")
UIManager:scheduleIn(2, connectionChangedEvent, complete_callback)
end
function NetworkMgr:isWifiOn()
return G_reader_settings:nilOrTrue("emulator_fake_wifi_connected")
end
NetworkMgr.isConnected = NetworkMgr.isWifiOn
end
io.write("Starting SDL in " .. SDL.getBasePath() .. "\n")
-------------- device probe ------------
if os.getenv("APPIMAGE") then
return AppImage
elseif os.getenv("FLATPAK") then
return Flatpak
elseif os.getenv("KO_MULTIUSER") then
return Desktop
elseif os.getenv("UBUNTU_APPLICATION_ISOLATION") then
return UbuntuTouch
else
return Emulator
end