Add IME support (#991)
This commit is contained in:
parent
7107f88f9f
commit
5c2c95765e
|
@ -456,6 +456,18 @@ function Doc:text_input(text, idx)
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
function Doc:ime_text_editing(text, start, length, idx)
|
||||
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
|
||||
if line1 ~= line2 or col1 ~= col2 then
|
||||
self:delete_to_cursor(sidx)
|
||||
end
|
||||
self:insert(line1, col1, text)
|
||||
self:set_selections(sidx, line1, col1 + #text, line1, col1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
|
||||
local old_text = self:get_text(line1, col1, line2, col2)
|
||||
local new_text, res = fn(old_text)
|
||||
|
|
|
@ -4,6 +4,7 @@ local config = require "core.config"
|
|||
local style = require "core.style"
|
||||
local keymap = require "core.keymap"
|
||||
local translate = require "core.doc.translate"
|
||||
local ime = require "core.ime"
|
||||
local View = require "core.view"
|
||||
|
||||
---@class core.docview : core.view
|
||||
|
@ -60,6 +61,7 @@ function DocView:new(doc)
|
|||
self.doc = assert(doc)
|
||||
self.font = "code_font"
|
||||
self.last_x_offset = {}
|
||||
self.ime_selection = { from = 0, size = 0 }
|
||||
end
|
||||
|
||||
|
||||
|
@ -292,13 +294,42 @@ function DocView:on_text_input(text)
|
|||
self.doc:text_input(text)
|
||||
end
|
||||
|
||||
function DocView:on_ime_text_editing(text, start, length)
|
||||
self.doc:ime_text_editing(text, start, length)
|
||||
self.ime_selection.from = start
|
||||
self.ime_selection.size = length
|
||||
|
||||
-- Set the composition bounding box that the system IME
|
||||
-- will consider when drawing its interface
|
||||
local line1, col1, line2, col2 = self.doc:get_selection(true)
|
||||
local x, y = self:get_line_screen_position(line1)
|
||||
local h = self:get_line_height()
|
||||
local col = math.min(col1, col2)
|
||||
|
||||
local x1, x2 = 0, 0
|
||||
|
||||
if length > 0 then
|
||||
-- focus on a part of the text
|
||||
local from = col + start
|
||||
local to = from + length
|
||||
x1 = self:get_col_x_offset(line1, from)
|
||||
x2 = self:get_col_x_offset(line1, to)
|
||||
else
|
||||
-- focus the whole text
|
||||
x1 = self:get_col_x_offset(line1, col1)
|
||||
x2 = self:get_col_x_offset(line2, col2)
|
||||
end
|
||||
|
||||
ime.set_location(x + x1, y, x2 - x1, h)
|
||||
self:scroll_to_make_visible(line1, col + start)
|
||||
end
|
||||
|
||||
function DocView:update()
|
||||
-- scroll to make caret visible and reset blink timer if it moved
|
||||
local line1, col1, line2, col2 = self.doc:get_selection()
|
||||
if (line1 ~= self.last_line1 or col1 ~= self.last_col1 or
|
||||
line2 ~= self.last_line2 or col2 ~= self.last_col2) and self.size.x > 0 then
|
||||
if core.active_view == self then
|
||||
if core.active_view == self and not ime.editing then
|
||||
self:scroll_to_make_visible(line1, col1)
|
||||
end
|
||||
core.blink_reset()
|
||||
|
@ -399,17 +430,45 @@ function DocView:draw_line_gutter(line, x, y, width)
|
|||
end
|
||||
|
||||
|
||||
function DocView:draw_ime_decoration(line1, col1, line2, col2)
|
||||
local x, y = self:get_line_screen_position(line1)
|
||||
local line_size = math.max(1, SCALE)
|
||||
local lh = self:get_line_height()
|
||||
|
||||
-- Draw IME underline
|
||||
local x1 = self:get_col_x_offset(line1, col1)
|
||||
local x2 = self:get_col_x_offset(line2, col2)
|
||||
renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.text)
|
||||
|
||||
-- Draw IME selection
|
||||
local col = math.min(col1, col2)
|
||||
local from = col + self.ime_selection.from
|
||||
local to = from + self.ime_selection.size
|
||||
x1 = self:get_col_x_offset(line1, from)
|
||||
if from ~= to then
|
||||
x2 = self:get_col_x_offset(line1, to)
|
||||
line_size = style.caret_width
|
||||
renderer.draw_rect(x + math.min(x1, x2), y + lh - line_size, math.abs(x1 - x2), line_size, style.caret)
|
||||
end
|
||||
self:draw_caret(x + x1, y)
|
||||
end
|
||||
|
||||
|
||||
function DocView:draw_overlay()
|
||||
if core.active_view == self then
|
||||
local minline, maxline = self:get_visible_line_range()
|
||||
-- draw caret if it overlaps this line
|
||||
local T = config.blink_period
|
||||
for _, line, col in self.doc:get_selections() do
|
||||
if line >= minline and line <= maxline
|
||||
for _, line1, col1, line2, col2 in self.doc:get_selections() do
|
||||
if line1 >= minline and line1 <= maxline
|
||||
and system.window_has_focus() then
|
||||
if config.disable_blink
|
||||
or (core.blink_timer - core.blink_start) % T < T / 2 then
|
||||
self:draw_caret(self:get_line_screen_position(line, col))
|
||||
if ime.editing then
|
||||
self:draw_ime_decoration(line1, col1, line2, col2)
|
||||
else
|
||||
if config.disable_blink
|
||||
or (core.blink_timer - core.blink_start) % T < T / 2 then
|
||||
self:draw_caret(self:get_line_screen_position(line1, col1))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
local core = require "core"
|
||||
|
||||
local ime = { }
|
||||
|
||||
function ime.reset()
|
||||
ime.editing = false
|
||||
end
|
||||
|
||||
---Convert from utf-8 offset and length (from SDL) to byte offsets
|
||||
---@param text string @Textediting string
|
||||
---@param start integer @0-based utf-8 offset of the starting position of the selection
|
||||
---@param length integer @Size of the utf-8 length of the selection
|
||||
function ime.ingest(text, start, length)
|
||||
if #text == 0 then
|
||||
-- finished textediting
|
||||
ime.reset()
|
||||
return "", 0, 0
|
||||
end
|
||||
|
||||
ime.editing = true
|
||||
|
||||
if start < 0 then
|
||||
-- we assume no selection and caret at the end
|
||||
return text, #text, 0
|
||||
end
|
||||
|
||||
-- start is 0-based, so we use start + 1
|
||||
local start_byte = utf8.offset(text, start + 1)
|
||||
if not start_byte then
|
||||
-- bad start offset
|
||||
-- we assume it meant the last byte of the text
|
||||
start_byte = #text
|
||||
else
|
||||
start_byte = math.min(start_byte - 1, #text)
|
||||
end
|
||||
|
||||
if length < 0 then
|
||||
-- caret only
|
||||
return text, start_byte, 0
|
||||
end
|
||||
|
||||
local end_byte = utf8.offset(text, start + length + 1)
|
||||
if not end_byte or end_byte - 1 < start_byte then
|
||||
-- bad length, assume caret only
|
||||
return text, start_byte, 0
|
||||
end
|
||||
|
||||
end_byte = math.min(end_byte - 1, #text)
|
||||
return text, start_byte, end_byte - start_byte
|
||||
end
|
||||
|
||||
---Forward the given textediting SDL event data to Views.
|
||||
---@param text string @Textediting string
|
||||
---@param start integer @0-based utf-8 offset of the starting position of the selection
|
||||
---@param length integer @Size of the utf-8 length of the selection
|
||||
function ime.on_text_editing(text, start, length, ...)
|
||||
if ime.editing or #text > 0 then
|
||||
core.root_view:on_ime_text_editing(ime.ingest(text, start, length, ...))
|
||||
end
|
||||
end
|
||||
|
||||
---Stop IME composition.
|
||||
---Might not completely work on every platform.
|
||||
function ime.stop()
|
||||
if ime.editing then
|
||||
-- SDL_ClearComposition for now doesn't work everywhere
|
||||
system.clear_ime()
|
||||
ime.on_text_editing("", 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
---Set the bounding box of the text pertaining the IME.
|
||||
---The IME will draw its interface based on this info.
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param w number
|
||||
---@param h number
|
||||
function ime.set_location(x, y, w, h)
|
||||
system.set_text_input_rect(x, y, w, h)
|
||||
end
|
||||
|
||||
ime.reset()
|
||||
return ime
|
|
@ -6,6 +6,7 @@ local style = require "colors.default"
|
|||
local command
|
||||
local keymap
|
||||
local dirwatch
|
||||
local ime
|
||||
local RootView
|
||||
local StatusView
|
||||
local TitleView
|
||||
|
@ -657,6 +658,7 @@ function core.init()
|
|||
command = require "core.command"
|
||||
keymap = require "core.keymap"
|
||||
dirwatch = require "core.dirwatch"
|
||||
ime = require "core.ime"
|
||||
RootView = require "core.rootview"
|
||||
StatusView = require "core.statusview"
|
||||
TitleView = require "core.titleview"
|
||||
|
@ -1051,6 +1053,8 @@ end
|
|||
|
||||
function core.set_active_view(view)
|
||||
assert(view, "Tried to set active view to nil")
|
||||
-- Reset the IME even if the focus didn't change
|
||||
ime.stop()
|
||||
if view ~= core.active_view then
|
||||
if core.active_view and core.active_view.force_focus then
|
||||
core.next_active_view = view
|
||||
|
@ -1215,6 +1219,8 @@ function core.on_event(type, ...)
|
|||
local did_keymap = false
|
||||
if type == "textinput" then
|
||||
core.root_view:on_text_input(...)
|
||||
elseif type == "textediting" then
|
||||
ime.on_text_editing(...)
|
||||
elseif type == "keypressed" then
|
||||
did_keymap = keymap.on_key_pressed(...)
|
||||
elseif type == "keyreleased" then
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
local core = require "core"
|
||||
local command = require "core.command"
|
||||
local config = require "core.config"
|
||||
local ime = require "core.ime"
|
||||
local keymap = {}
|
||||
|
||||
---@alias keymap.shortcut string
|
||||
|
@ -177,6 +178,10 @@ end
|
|||
-- Events listening
|
||||
--------------------------------------------------------------------------------
|
||||
function keymap.on_key_pressed(k, ...)
|
||||
-- In Windows during IME composition, input is still sent to us
|
||||
-- so we just ignore it
|
||||
if ime.editing then return false end
|
||||
|
||||
local mk = modkey_map[k]
|
||||
if mk then
|
||||
keymap.modkeys[mk] = true
|
||||
|
|
|
@ -334,6 +334,9 @@ function RootView:on_text_input(...)
|
|||
core.active_view:on_text_input(...)
|
||||
end
|
||||
|
||||
function RootView:on_ime_text_editing(...)
|
||||
core.active_view:on_ime_text_editing(...)
|
||||
end
|
||||
|
||||
function RootView:on_focus_lost(...)
|
||||
-- We force a redraw so documents can redraw without the cursor.
|
||||
|
|
|
@ -238,6 +238,10 @@ function View:on_text_input(text)
|
|||
-- no-op
|
||||
end
|
||||
|
||||
function View:on_ime_text_editing(text, start, length)
|
||||
-- no-op
|
||||
end
|
||||
|
||||
---@param y number
|
||||
---@return boolean
|
||||
function View:on_mouse_wheel(y)
|
||||
|
|
|
@ -249,6 +249,23 @@ top:
|
|||
lua_pushstring(L, e.text.text);
|
||||
return 2;
|
||||
|
||||
case SDL_TEXTEDITING:
|
||||
lua_pushstring(L, "textediting");
|
||||
lua_pushstring(L, e.edit.text);
|
||||
lua_pushinteger(L, e.edit.start);
|
||||
lua_pushinteger(L, e.edit.length);
|
||||
return 4;
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 22)
|
||||
case SDL_TEXTEDITING_EXT:
|
||||
lua_pushstring(L, "textediting");
|
||||
lua_pushstring(L, e.editExt.text);
|
||||
lua_pushinteger(L, e.editExt.start);
|
||||
lua_pushinteger(L, e.editExt.length);
|
||||
SDL_free(e.editExt.text);
|
||||
return 4;
|
||||
#endif
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
if (e.button.button == 1) { SDL_CaptureMouse(1); }
|
||||
lua_pushstring(L, "mousepressed");
|
||||
|
@ -425,6 +442,23 @@ static int f_get_window_mode(lua_State *L) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
static int f_set_text_input_rect(lua_State *L) {
|
||||
SDL_Rect rect;
|
||||
rect.x = luaL_checknumber(L, 1);
|
||||
rect.y = luaL_checknumber(L, 2);
|
||||
rect.w = luaL_checknumber(L, 3);
|
||||
rect.h = luaL_checknumber(L, 4);
|
||||
SDL_SetTextInputRect(&rect);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int f_clear_ime(lua_State *L) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 22)
|
||||
SDL_ClearComposition();
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int f_raise_window(lua_State *L) {
|
||||
/*
|
||||
|
@ -986,6 +1020,8 @@ static const luaL_Reg lib[] = {
|
|||
{ "set_window_hit_test", f_set_window_hit_test },
|
||||
{ "get_window_size", f_get_window_size },
|
||||
{ "set_window_size", f_set_window_size },
|
||||
{ "set_text_input_rect", f_set_text_input_rect },
|
||||
{ "clear_ime", f_clear_ime },
|
||||
{ "window_has_focus", f_window_has_focus },
|
||||
{ "raise_window", f_raise_window },
|
||||
{ "show_fatal_error", f_show_fatal_error },
|
||||
|
|
|
@ -133,6 +133,12 @@ int main(int argc, char **argv) {
|
|||
#if SDL_VERSION_ATLEAST(2, 0, 5)
|
||||
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||
#endif
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
|
||||
#endif
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 22)
|
||||
SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1");
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 8)
|
||||
/* This hint tells SDL to respect borderless window as a normal window.
|
||||
|
|
Loading…
Reference in New Issue