mirror of
https://github.com/koreader/koreader
synced 2024-11-13 19:11:25 +00:00
13a521c398
SDL supports multitouch since https://github.com/koreader/koreader-base/pull/1599
446 lines
15 KiB
Lua
446 lines
15 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,
|
|
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", {}),
|
|
}
|
|
|
|
local AppImage = Device:extend{
|
|
model = "AppImage",
|
|
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,
|
|
-- 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()
|
|
-- 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()
|
|
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
|
|
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("KO_MULTIUSER") then
|
|
return Desktop
|
|
elseif os.getenv("UBUNTU_APPLICATION_ISOLATION") then
|
|
return UbuntuTouch
|
|
else
|
|
return Emulator
|
|
end
|