Replace the prompt with a proper text input widget

This commit is contained in:
Ksaper 2023-03-07 13:12:21 +02:00
parent 13d0f1c435
commit 4cdc4fb5f9
3 changed files with 801 additions and 530 deletions

View File

@ -5,7 +5,7 @@ local gtable = require("gears.table")
local gtimer = require("gears.timer")
local wibox = require("wibox")
local beautiful = require("beautiful")
local prompt_widget = require(... .. ".prompt")
local text_input_widget = require(... .. ".text_input")
local dpi = beautiful.xresources.apply_dpi
local string = string
local table = table
@ -318,22 +318,24 @@ end
local function build_widget(self)
local widget = self.widget_template
if widget == nil then
self._private.prompt = wibox.widget
self._private.text_input = wibox.widget
{
widget = prompt_widget,
always_on = true,
widget = text_input_widget,
reset_on_stop = self.reset_on_hide,
icon_font = self.prompt_icon_font,
icon_size = self.prompt_icon_size,
icon_color = self.prompt_icon_color,
icon = self.prompt_icon,
label_font = self.prompt_label_font,
label_size = self.prompt_label_size,
label_color = self.prompt_label_color,
label = self.prompt_label,
text_font = self.prompt_text_font,
text_size = self.prompt_text_size,
text_color = self.prompt_text_color,
placeholder = self.text_input_placeholder,
widget_template = wibox.widget {
widget = wibox.container.background,
forced_height = dpi(120),
bg = self.text_input_bg_color,
{
widget = wibox.container.margin,
margins = dpi(30),
{
widget = wibox.widget.textbox,
id = "text_role"
}
}
}
}
self._private.grid = wibox.widget
{
@ -347,21 +349,7 @@ local function build_widget(self)
widget = wibox.widget
{
layout = wibox.layout.fixed.vertical,
{
widget = wibox.container.background,
forced_height = dpi(120),
bg = self.prompt_bg_color,
{
widget = wibox.container.margin,
margins = dpi(30),
{
widget = wibox.container.place,
halign = "left",
valign = "center",
self._private.prompt
}
}
},
self._private.text_input,
{
widget = wibox.container.margin,
margins = dpi(30),
@ -369,7 +357,7 @@ local function build_widget(self)
}
}
else
self._private.prompt = widget:get_children_by_id("prompt_role")[1]
self._private.text_input = widget:get_children_by_id("text_input_role")[1]
self._private.grid = widget:get_children_by_id("grid_role")[1]
end
@ -403,7 +391,7 @@ local function build_widget(self)
end
end)
self:get_prompt():connect_signal("text::changed", function(_, text)
self:get_text_input():connect_signal("property::text", function(_, text)
if text == self:get_text() then
return
end
@ -412,10 +400,14 @@ local function build_widget(self)
self._private.search_timer:again()
end)
self:get_prompt():connect_signal("key::release", function(_, mod, key, cmd)
self:get_text_input():connect_signal("key::press", function(_, mod, key, cmd)
if key == "Escape" then
self:hide()
end
end)
self:get_text_input():connect_signal("key::release", function(_, mod, key, cmd)
if key == "Return" then
if self:get_selected_app_widget() ~= nil then
self:get_selected_app_widget():run()
@ -664,7 +656,7 @@ function app_launcher:show()
end
self:get_widget().visible = true
self:get_prompt():start()
self:get_text_input():focus()
self:emit_signal("visibility", true)
end
@ -678,7 +670,7 @@ function app_launcher:hide()
end
self:get_widget().visible = false
self:get_prompt():stop()
self:get_text_input():unfocus()
self:emit_signal("visibility", false)
end
@ -709,15 +701,15 @@ function app_launcher:reset()
local app = self:get_grid():get_widgets_at(1, 1)[1]
app:select()
self:get_prompt():set_text("")
self:get_text_input():set_text("")
end
function app_launcher:get_widget()
return self._private.widget
end
function app_launcher:get_prompt()
return self._private.prompt
function app_launcher:get_text_input()
return self._private.text_input
end
function app_launcher:get_grid()
@ -777,18 +769,8 @@ local function new(args)
args.apps_per_row = default_value(args.apps_per_row, 5)
args.apps_per_column = default_value(args.apps_per_column, 3)
args.prompt_bg_color = default_value(args.prompt_bg_color, "#000000")
args.prompt_icon_font = default_value(args.prompt_icon_font, beautiful.font)
args.prompt_icon_size = default_value(args.prompt_icon_size, 12)
args.prompt_icon_color = default_value(args.prompt_icon_color, "#FFFFFF")
args.prompt_icon = default_value(args.prompt_icon, "")
args.prompt_label_font = default_value(args.prompt_label_font, beautiful.font)
args.prompt_label_size = default_value(args.prompt_label_size, 12)
args.prompt_label_color = default_value(args.prompt_label_color, "#FFFFFF")
args.prompt_label = default_value(args.prompt_label, "<b>Search</b>: ")
args.prompt_text_font = default_value(args.prompt_text_font, beautiful.font)
args.prompt_text_size = default_value(args.prompt_text_size, 12)
args.prompt_text_color = default_value(args.prompt_text_color, "#FFFFFF")
args.text_input_bg_color = default_value(args.text_input_bg_color, "#000000")
args.text_input_placeholder = default_value(args.text_input_placeholder, "Search: ")
args.app_normal_color = default_value(args.app_normal_color, "#000000")
args.app_selected_color = default_value(args.app_selected_color, "#FFFFFF")

View File

@ -1,480 +0,0 @@
-------------------------------------------
-- @author https://github.com/Kasper24
-- @copyright 2021-2022 Kasper24
-------------------------------------------
local lgi = require('lgi')
local Gtk = lgi.require('Gtk', '3.0')
local Gdk = lgi.require('Gdk', '3.0')
local awful = require("awful")
local gtable = require("gears.table")
local gstring = require("gears.string")
local wibox = require("wibox")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi
local tostring = tostring
local tonumber = tonumber
local ceil = math.ceil
local ipairs = ipairs
local string = string
local type = type
local capi = {
awesome = awesome,
root = root,
mouse = mouse,
tag = tag,
client = client
}
local prompt = {
mt = {}
}
local properties = {
"only_numbers", "round", "obscure",
"always_on", "reset_on_stop",
"stop_on_lost_focus", "stop_on_tag_changed", "stop_on_clicked_outside",
"icon_font", "icon_size", "icon_color", "icon",
"label_font", "label_size", "label_color", "label",
"text_font", "text_size", "text_color", "text",
"cursor_size", "cursor_color"
}
local function is_word_char(c)
if string.find(c, "[{[(,.:;_-+=@/ ]") then
return false
else
return true
end
end
local function cword_start(s, pos)
local i = pos
if i > 1 then
i = i - 1
end
while i >= 1 and not is_word_char(s:sub(i, i)) do
i = i - 1
end
while i >= 1 and is_word_char(s:sub(i, i)) do
i = i - 1
end
if i <= #s then
i = i + 1
end
return i
end
local function cword_end(s, pos)
local i = pos
while i <= #s and not is_word_char(s:sub(i, i)) do
i = i + 1
end
while i <= #s and is_word_char(s:sub(i, i)) do
i = i + 1
end
return i
end
local function have_multibyte_char_at(text, position)
return text:sub(position, position):wlen() == -1
end
local function generate_markup(self)
local wp = self._private
local label_size = dpi(ceil(wp.label_size * 1024))
local text_size = dpi(ceil(wp.text_size * 1024))
local cursor_size = dpi(ceil(wp.cursor_size * 1024))
local text = tostring(wp.text) or ""
if wp.obscure == true then
text = text:gsub(".", "*")
end
local markup = ""
if wp.icon ~= nil then
if type(wp.icon) == "table" then
local icon_size = dpi(ceil(wp.icon.size * 1024))
markup = string.format(
'<span font_family="%s" font_size="%s" foreground="%s">%s </span>',
wp.icon.font, icon_size, wp.icon.color, wp.icon.icon)
else
local icon_size = dpi(ceil(wp.icon_size * 1024))
markup = string.format(
'<span font_family="%s" font_size="%s" foreground="%s">%s </span>',
wp.icon_font, icon_size, wp.icon_color, wp.icon)
end
end
if self._private.state == true then
local char, spacer, text_start, text_end
if #text < wp.cur_pos then
char = " "
spacer = ""
text_start = gstring.xml_escape(text)
text_end = ""
else
local offset = 0
if have_multibyte_char_at(text, wp.cur_pos) then
offset = 1
end
char = gstring.xml_escape(text:sub(wp.cur_pos, wp.cur_pos + offset))
spacer = " "
text_start = gstring.xml_escape(text:sub(1, wp.cur_pos - 1))
text_end = gstring.xml_escape(text:sub(wp.cur_pos + offset))
end
markup = markup .. (string.format(
'<span font_family="%s" font_size="%s" foreground="%s">%s</span>' ..
'<span font_family="%s" font_size="%s" foreground="%s">%s</span>' ..
'<span font_size="%s" background="%s">%s</span>' ..
'<span font_family="%s" font_size="%s" foreground="%s">%s%s</span>',
wp.label_font, label_size, wp.label_color, wp.label,
wp.text_font, text_size, wp.text_color, text_start,
cursor_size, wp.cursor_color, char,
wp.text_font, text_size, wp.text_color, text_end,
spacer))
else
markup = markup .. string.format(
'<span font_family="%s" font_size="%s" foreground="%s">%s</span>' ..
'<span font_family="%s" font_size="%s" foreground="%s">%s</span>',
wp.label_font, label_size, wp.label_color, wp.label,
wp.text_font, text_size, wp.text_color, gstring.xml_escape(text))
end
self:set_markup(markup)
end
local function paste(self)
local wp = self._private
wp.clipboard:request_text(function(clipboard, text)
if text then
wp.text = wp.text:sub(1, wp.cur_pos - 1) .. stdout .. self.text:sub(wp.cur_pos)
wp.cur_pos = wp.cur_pos + #stdout
generate_markup(self)
end
end)
end
local function build_properties(prototype, prop_names)
for _, prop in ipairs(prop_names) do
if not prototype["set_" .. prop] then
prototype["set_" .. prop] = function(self, value)
if self._private[prop] ~= value then
self._private[prop] = value
self:emit_signal("widget::redraw_needed")
self:emit_signal("property::" .. prop, value)
generate_markup(self)
end
return self
end
end
if not prototype["get_" .. prop] then
prototype["get_" .. prop] = function(self)
return self._private[prop]
end
end
end
end
function prompt:toggle_obscure()
self:set_obscure(not self._private.obscure)
end
function prompt:set_text(text)
self._private.text = text
self._private.cur_pos = #text + 1
generate_markup(self)
end
function prompt:get_text()
return self._private.text
end
function prompt:start()
local wp = self._private
wp.state = true
capi.awesome.emit_signal("prompt::toggled_on", self)
generate_markup(self)
wp.grabber = awful.keygrabber.run(function(modifiers, key, event)
-- Convert index array to hash table
local mod = {}
for _, v in ipairs(modifiers) do
mod[v] = true
end
if event ~= "press" then
self:emit_signal("key::release", mod, key, wp.text)
return
end
self:emit_signal("key::press", mod, key, wp.text)
-- Control cases
if mod.Control then
if key == "v" then
paste(self)
elseif key == "a" then
wp.cur_pos = 1
elseif key == "b" then
if wp.cur_pos > 1 then
wp.cur_pos = wp.cur_pos - 1
if have_multibyte_char_at(wp.text, wp.cur_pos) then
wp.cur_pos = wp.cur_pos - 1
end
end
elseif key == "d" then
if wp.cur_pos <= #wp.text then
wp.text = wp.text:sub(1, wp.cur_pos - 1) .. wp.text:sub(wp.cur_pos + 1)
end
elseif key == "e" then
wp.cur_pos = #wp.text + 1
elseif key == "f" then
if wp.cur_pos <= #wp.text then
if have_multibyte_char_at(wp.text, wp.cur_pos) then
wp.cur_pos = wp.cur_pos + 2
else
wp.cur_pos = wp.cur_pos + 1
end
end
elseif key == "h" then
if wp.cur_pos > 1 then
local offset = 0
if have_multibyte_char_at(wp.text, wp.cur_pos - 1) then
offset = 1
end
wp.text = wp.text:sub(1, wp.cur_pos - 2 - offset) .. wp.text:sub(wp.cur_pos)
wp.cur_pos = wp.cur_pos - 1 - offset
end
elseif key == "k" then
wp.text = wp.text:sub(1, wp.cur_pos - 1)
elseif key == "u" then
wp.text = wp.text:sub(wp.cur_pos, #wp.text)
wp.cur_pos = 1
elseif key == "w" or key == "BackSpace" then
local wstart = 1
local wend = 1
local cword_start_pos = 1
local cword_end_pos = 1
while wend < wp.cur_pos do
wend = wp.text:find("[{[(,.:;_-+=@/ ]", wstart)
if not wend then
wend = #wp.text + 1
end
if wp.cur_pos >= wstart and wp.cur_pos <= wend + 1 then
cword_start_pos = wstart
cword_end_pos = wp.cur_pos - 1
break
end
wstart = wend + 1
end
wp.text = wp.text:sub(1, cword_start_pos - 1) .. wp.text:sub(cword_end_pos + 1)
wp.cur_pos = cword_start_pos
end
elseif mod.Mod1 or mod.Mod3 then
if key == "b" then
wp.cur_pos = cword_start(wp.text, wp.cur_pos)
elseif key == "f" then
wp.cur_pos = cword_end(wp.text, wp.cur_pos)
elseif key == "d" then
wp.text = wp.text:sub(1, wp.cur_pos - 1) .. wp.text:sub(cword_end(wp.text, wp.cur_pos))
elseif key == "BackSpace" then
local wstart = cword_start(wp.text, wp.cur_pos)
wp.text = wp.text:sub(1, wstart - 1) .. wp.text:sub(wp.cur_pos)
wp.cur_pos = wstart
end
else
if key == "Escape" or key == "Return" then
if self.always_on == false then
self:stop()
return
end
elseif mod.Shift and key == "Insert" then
paste(self)
elseif key == "Home" then
wp.cur_pos = 1
elseif key == "End" then
wp.cur_pos = #wp.text + 1
elseif key == "BackSpace" then
if wp.cur_pos > 1 then
local offset = 0
if have_multibyte_char_at(wp.text, wp.cur_pos - 1) then
offset = 1
end
wp.text = wp.text:sub(1, wp.cur_pos - 2 - offset) .. wp.text:sub(wp.cur_pos)
wp.cur_pos = wp.cur_pos - 1 - offset
end
elseif key == "Delete" then
wp.text = wp.text:sub(1, wp.cur_pos - 1) .. wp.text:sub(wp.cur_pos + 1)
elseif key == "Left" then
wp.cur_pos = wp.cur_pos - 1
elseif key == "Right" then
wp.cur_pos = wp.cur_pos + 1
else
if wp.round and key == "." then
return
end
if wp.only_numbers and tonumber(wp.text .. key) == nil then
return
end
-- wlen() is UTF-8 aware but #key is not,
-- so check that we have one UTF-8 char but advance the cursor of # position
if key:wlen() == 1 then
wp.text = wp.text:sub(1, wp.cur_pos - 1) .. key .. wp.text:sub(wp.cur_pos)
wp.cur_pos = wp.cur_pos + #key
end
end
if wp.cur_pos < 1 then
wp.cur_pos = 1
elseif wp.cur_pos > #wp.text + 1 then
wp.cur_pos = #wp.text + 1
end
end
if wp.only_numbers and wp.text == "" then
wp.text = "0"
wp.cur_pos = #wp.text + 1
end
generate_markup(self)
self:emit_signal("text::changed", wp.text)
end)
end
function prompt:stop()
local wp = self._private
if wp.state == false then
return
end
wp.state = false
if self.reset_on_stop == true then
self:set_text("")
end
if wp.grabber then
awful.keygrabber.stop(wp.grabber)
end
generate_markup(self)
self:emit_signal("stopped", wp.text)
end
function prompt:toggle()
local wp = self._private
if wp.state == true then
self:stop()
else
self:start()
end
end
local function new()
local widget = wibox.widget.textbox()
gtable.crush(widget, prompt, true)
local wp = widget._private
wp.only_numbers = false
wp.round = false
wp.always_on = false
wp.reset_on_stop = false
wp.obscure = false
wp.stop_on_focus_lost = false
wp.stop_on_tag_changed = false
wp.stop_on_clicked_outside = true
wp.icon_font = beautiful.font
wp.icon_size = 12
wp.icon_color = beautiful.colors.on_background
wp.icon = nil
wp.label_font = beautiful.font
wp.label_size = 12
wp.label_color = beautiful.colors.on_background
wp.label = ""
wp.text_font = beautiful.font
wp.text_size = 12
wp.text_color = beautiful.colors.on_background
wp.text = ""
wp.cursor_size = 4
wp.cursor_color = beautiful.colors.on_background
wp.cur_pos = #wp.text + 1 or 1
wp.state = false
wp.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
widget:connect_signal("mouse::enter", function(self, find_widgets_result)
capi.root.cursor("xterm")
local wibox = capi.mouse.current_wibox
if wibox then
wibox.cursor = "xterm"
end
end)
widget:connect_signal("mouse::leave", function()
capi.root.cursor("left_ptr")
local wibox = capi.mouse.current_wibox
if wibox then
wibox.cursor = "left_ptr"
end
if wp.stop_on_focus_lost ~= false and wp.always_on == false and wp.state == true then
widget:stop()
end
end)
widget:connect_signal("button::press", function(self, lx, ly, button, mods, find_widgets_result)
if wp.always_on then
return
end
if button == 1 then
widget:toggle()
end
end)
-- TODO make it work outside my config
capi.awesome.connect_signal("root::pressed", function()
if wp.stop_on_clicked_outside ~= false and wp.always_on == false and wp.state == true then
widget:stop()
end
end)
capi.client.connect_signal("button::press", function()
if wp.stop_on_clicked_outside ~= false and wp.always_on == false and wp.state == true then
widget:stop()
end
end)
capi.tag.connect_signal("property::selected", function()
if wp.stop_on_tag_changed ~= false and wp.always_on == false and wp.state == true then
widget:stop()
end
end)
capi.awesome.connect_signal("prompt::toggled_on", function(prompt)
if wp.always_on == false and prompt ~= widget and wp.state == true then
widget:stop()
end
end)
return widget
end
function prompt.mt:__call(...)
return new(...)
end
build_properties(prompt, properties)
return setmetatable(prompt, prompt.mt)

View File

@ -0,0 +1,769 @@
-------------------------------------------
-- @author https://github.com/Kasper24
-- @copyright 2021-2022 Kasper24
-------------------------------------------
local lgi = require('lgi')
local Gtk = lgi.require('Gtk', '3.0')
local Gdk = lgi.require('Gdk', '3.0')
local Pango = lgi.Pango
local awful = require("awful")
local gtable = require("gears.table")
local gtimer = require("gears.timer")
local gcolor = require("gears.color")
local wibox = require("wibox")
local beautiful = require("beautiful")
local tonumber = tonumber
local ipairs = ipairs
local string = string
local capi = {
awesome = awesome,
root = root,
tag = tag,
client = client,
mouse = mouse,
mousegrabber = mousegrabber
}
local text_input = {
mt = {}
}
local properties = {
"unfocus_keys", "unfocus_on_clicked_inside", "unfocus_on_clicked_outside", "unfocus_on_mouse_leave", "unfocus_on_tag_change",
"focus_on_subject_mouse_enter", "unfocus_on_subject_mouse_leave",
"reset_on_unfocus",
"placeholder", "text", "only_numbers", "round", "obscure",
"cursor_blink", "cursor_blink_rate","cursor_size", "cursor_bg",
"selection_bg"
}
local function build_properties(prototype, prop_names)
for _, prop in ipairs(prop_names) do
if not prototype["set_" .. prop] then
prototype["set_" .. prop] = function(self, value)
if self._private[prop] ~= value then
self._private[prop] = value
self:emit_signal("widget::redraw_needed")
self:emit_signal("property::" .. prop, value)
end
return self
end
end
if not prototype["get_" .. prop] then
prototype["get_" .. prop] = function(self)
return self._private[prop]
end
end
end
end
local function has_value(tab, val)
for _, value in ipairs(tab) do
if val:lower():find(value:lower(), 1, true) then
return true
end
end
return false
end
local function is_word_char(c)
if string.find(c, "[{[(,.:;_-+=@/ ]") then
return false
else
return true
end
end
local function cword_start(s, pos)
local i = pos
if i > 1 then
i = i - 1
end
while i >= 1 and not is_word_char(s:sub(i, i)) do
i = i - 1
end
while i >= 1 and is_word_char(s:sub(i, i)) do
i = i - 1
end
if i <= #s then
i = i + 1
end
return i
end
local function cword_end(s, pos)
local i = pos
while i <= #s and not is_word_char(s:sub(i, i)) do
i = i + 1
end
while i <= #s and is_word_char(s:sub(i, i)) do
i = i + 1
end
return i
end
local function run_mousegrabber(self)
capi.mousegrabber.run(function(m)
if m.buttons[1] then
if capi.mouse.current_widget ~= self and self.unfocus_on_clicked_outside then
self:unfocus()
return false
elseif capi.mouse.current_widget == self and self.unfocus_on_clicked_inside then
self:unfocus()
return false
end
end
return true
end, "xterm")
end
local function run_keygrabber(self)
local wp = self._private
wp.keygrabber = awful.keygrabber.run(function(modifiers, key, event)
if event ~= "press" then
self:emit_signal("key::release", modifiers, key, event)
return
end
self:emit_signal("key::press", modifiers, key, event)
-- Convert index array to hash table
local mod = {}
for _, v in ipairs(modifiers) do
mod[v] = true
end
if mod.Control then
if key == "a" then
self:select_all()
elseif key == "c" then
self:copy()
elseif key == "v" then
self:paste()
elseif key == "b" or key == "Left" then
self:set_cursor_index_to_word_start()
elseif key == "f" or key == "Right" then
self:set_cursor_index_to_word_end()
elseif key == "d" then
self:delete_next_word()
elseif key == "BackSpace" then
self:delete_previous_word()
end
elseif mod.Shift then
if key =="Left" then
self:decremeant_selection_end_index()
elseif key == "Right" then
self:increamant_selection_end_index()
end
else
if has_value(wp.unfocus_keys, key) then
self:unfocus()
end
if mod.Shift and key == "Insert" then
self:paste()
elseif key == "Home" then
self:set_cursor_index(0)
elseif key == "End" then
self:set_cursor_index_to_end()
elseif key == "BackSpace" then
self:delete_text()
elseif key == "Delete" then
self:delete_text_after_cursor()
elseif key == "Left" then
self:decremeant_cursor_index()
elseif key == "Right" then
self:increamant_cursor_index()
else
if (wp.round and key == ".") or (wp.only_numbers and tonumber(self:get_text() .. key) == nil) then
return
end
-- wlen() is UTF-8 aware but #key is not,
-- so check that we have one UTF-8 char but advance the cursor of # position
if key:wlen() == 1 then
self:update_text(key)
end
end
end
end)
end
function text_input:set_widget_template(widget_template)
local wp = self._private
self._private.text_widget = widget_template:get_children_by_id("text_role")[1]
self._private.text_widget.forced_width = math.huge
local text_draw = self._private.text_widget.draw
local placeholder_widget = widget_template:get_children_by_id("placeholder_role")
if placeholder_widget then
placeholder_widget = placeholder_widget[1]
end
function self._private.text_widget:draw(context, cr, width, height)
-- Selection bg
local ink_rect, logical_rect = self._private.layout:get_pixel_extents()
cr:set_source(gcolor.change_opacity(wp.selection_bg, wp.selection_opacity))
cr:rectangle(
wp.selection_start_x,
logical_rect.y - 3,
wp.selection_end_x - wp.selection_start_x,
logical_rect.y + logical_rect.height + 6
)
cr:fill()
-- Cursor
local ink_rect, logical_rect = self._private.layout:get_pixel_extents()
cr:set_source(gcolor.change_opacity(wp.cursor_bg, wp.cursor_opacity))
cr:set_line_width(wp.cursor_width)
cr:move_to(wp.cursor_x, logical_rect.y - 3)
cr:line_to(wp.cursor_x, logical_rect.y + logical_rect.height + 6)
cr:stroke()
cr:set_source_rgb(1, 1, 1)
text_draw(self, context, cr, width, height)
if self:get_text() == "" and placeholder_widget then
placeholder_widget.visible = true
elseif placeholder_widget then
placeholder_widget.visible = false
end
end
wp.selecting_text = false
local function on_drag(drawable, lx, ly)
if not wp.selecting_text and (lx ~= wp.press_pos.lx or ly ~= wp.press_pos.ly) then
self:set_selection_start_index_from_x_y(wp.press_pos.lx, wp.press_pos.ly)
self:set_selection_end_index(self._private.selection_start)
wp.selecting_text = true
elseif wp.selecting_text then
self:set_selection_end_index_from_x_y(lx - wp.offset.x, ly - wp.offset.y)
end
end
self._private.text_widget:connect_signal("button::press", function(_, lx, ly, button, mods, find_widgets_result)
if button == 1 then
self:focus()
wp.press_pos = { lx = lx, ly = ly }
wp.offset = { x = find_widgets_result.x, y = find_widgets_result.y }
find_widgets_result.drawable:connect_signal("mouse::move", on_drag)
end
end)
self._private.text_widget:connect_signal("button::release", function(_, lx, ly, button, mods, find_widgets_result)
find_widgets_result.drawable:disconnect_signal("mouse::move", on_drag)
if not wp.selecting_text then
self:set_cursor_index_from_x_y(lx, ly)
else
wp.selecting_text = false
end
end)
self._private.text_widget:connect_signal("mouse::enter", function()
capi.root.cursor("xterm")
local wibox = capi.mouse.current_wibox
if wibox then
wibox.cursor = "xterm"
end
end)
self._private.text_widget:connect_signal("mouse::leave", function(_, find_widgets_result)
if self:get_focused() == false then
capi.root.cursor("left_ptr")
local wibox = capi.mouse.current_wibox
if wibox then
wibox.cursor = "left_ptr"
end
end
find_widgets_result.drawable:disconnect_signal("mouse::move", on_drag)
if wp.unfocus_on_mouse_leave then
self:unfocus()
end
end)
self:set_widget(widget_template)
end
function text_input:get_mode()
return self._private.mode
end
function text_input:set_focused(focused)
if focused == true then
self:focus()
else
self:unfocus()
end
end
function text_input:toggle_obscure()
self:set_obscure(not self._private.obscure)
end
function text_input:update_text(text)
if self:get_mode() == "insert" then
self:insert_text(text)
else
self:overwrite_text(text)
end
end
function text_input:set_text(text)
local wp = self._private
local text_widget = self:get_text_widget()
text_widget:set_text(text)
if text_widget:get_text() == "" then
self:set_cursor_index(0)
else
self:set_cursor_index(#text)
end
self:emit_signal("property::text", text_widget:get_text())
end
function text_input:insert_text(text)
local old_text = self:get_text()
local cursor_index = self:get_cursor_index()
local left_text = old_text:sub(1, cursor_index) .. text
local right_text = old_text:sub(cursor_index + 1)
self:get_text_widget():set_text(left_text .. right_text)
self:set_cursor_index(self:get_cursor_index() + #text)
self:emit_signal("property::text", self:get_text())
end
function text_input:overwrite_text(text)
local start_pos = self._private.selection_start
local end_pos = self._private.selection_end
if start_pos > end_pos then
start_pos, end_pos = end_pos, start_pos
end
local old_text = self:get_text()
local left_text = old_text:sub(1, start_pos)
local right_text = old_text:sub(end_pos + 1)
self:get_text_widget():set_text(left_text .. text .. right_text)
self:set_cursor_index(#left_text)
self:emit_signal("property::text", self:get_text())
end
function text_input:copy()
local wp = self._private
if self:get_mode() == "overwrite" then
local text = self:get_text()
local start_pos = self._private.selection_start
local end_pos = self._private.selection_end
if start_pos > end_pos then
start_pos, end_pos = end_pos + 1, start_pos
end
text = text:sub(start_pos, end_pos)
wp.clipboard:set_text(text, -1)
end
end
function text_input:paste()
local wp = self._private
wp.clipboard:request_text(function(clipboard, text)
if text then
self:update_text(text)
end
end)
end
function text_input:delete_next_word()
local old_text = self:get_text()
local cursor_index = self:get_cursor_index()
local left_text = old_text:sub(1, cursor_index)
local right_text = old_text:sub(cword_end(old_text, cursor_index + 1))
self:get_text_widget():set_text(left_text .. right_text)
self:emit_signal("property::text", self:get_text())
end
function text_input:delete_previous_word()
local old_text = self:get_text()
local cursor_index = self:get_cursor_index()
local wstart = cword_start(old_text, cursor_index + 1) - 1
local left_text = old_text:sub(1, wstart)
local right_text = old_text:sub(cursor_index + 1)
self:get_text_widget():set_text(left_text .. right_text)
self:set_cursor_index(wstart)
self:emit_signal("property::text", self:get_text())
end
function text_input:delete_text()
if self:get_mode() == "insert" then
self:delete_text_before_cursor()
else
self:overwrite_text("")
end
end
function text_input:delete_text_before_cursor()
local cursor_index = self:get_cursor_index()
if cursor_index > 0 then
local old_text = self:get_text()
local left_text = old_text:sub(1, cursor_index - 1)
local right_text = old_text:sub(cursor_index + 1)
self:get_text_widget():set_text(left_text .. right_text)
self:set_cursor_index(cursor_index - 1)
self:emit_signal("property::text", self:get_text())
end
end
function text_input:delete_text_after_cursor()
local cursor_index = self:get_cursor_index()
if cursor_index < #self:get_text() then
local old_text = self:get_text()
local left_text = old_text:sub(1, cursor_index)
local right_text = old_text:sub(cursor_index + 2)
self:get_text_widget():set_text(left_text .. right_text)
self:emit_signal("property::text", self:get_text())
end
end
function text_input:get_text()
return self:get_text_widget():get_text()
end
function text_input:get_text_widget()
return self._private.text_widget
end
function text_input:show_selection()
self._private.selection_opacity = 1
self:get_text_widget():emit_signal("widget::redraw_needed")
end
function text_input:hide_selection()
self._private.selection_opacity = 0
self:get_text_widget():emit_signal("widget::redraw_needed")
end
function text_input:select_all()
self:set_selection_start_index(0)
self:set_selection_end_index(#self:get_text())
end
function text_input:set_selection_start_index(index)
index = math.max(math.min(index, #self:get_text()), 0)
local layout = self:get_text_widget()._private.layout
local strong_pos, weak_pos = layout:get_caret_pos(index)
if strong_pos then
self._private.selection_start = index
self._private.mode = "overwrite"
self._private.selection_start_x = strong_pos.x / Pango.SCALE
self._private.selection_start_y = strong_pos.y / Pango.SCALE
self:show_selection()
self:hide_cursor()
self:get_text_widget():emit_signal("widget::redraw_needed")
end
end
function text_input:set_selection_end_index(index)
index = math.max(math.min(index, #self:get_text()), 0)
local layout = self:get_text_widget()._private.layout
local strong_pos, weak_pos = layout:get_caret_pos(index)
if strong_pos then
self._private.selection_end_x = strong_pos.x / Pango.SCALE
self._private.selection_end_y = strong_pos.y / Pango.SCALE
self._private.selection_end = index
self:get_text_widget():emit_signal("widget::redraw_needed")
end
end
function text_input:increamant_selection_end_index()
if self:get_mode() == "insert" then
self:set_selection_start_index(self:get_cursor_index())
self:set_selection_end_index(self:get_cursor_index() + 1)
else
self:set_selection_end_index(self._private.selection_end + 1)
end
end
function text_input:decremeant_selection_end_index()
if self:get_mode() == "insert" then
self:set_selection_start_index(self:get_cursor_index())
self:set_selection_end_index(self:get_cursor_index() - 1)
else
self:set_selection_end_index(self._private.selection_end - 1)
end
end
function text_input:set_selection_start_index_from_x_y(x, y)
local layout = self:get_text_widget()._private.layout
local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
if index then
self:set_selection_start_index(index)
else
self:set_selection_start_index(#self:get_text())
end
end
function text_input:set_selection_end_index_from_x_y(x, y)
local layout = self:get_text_widget()._private.layout
local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
if index then
self:set_selection_end_index(index + trailing)
end
end
function text_input:show_cursor()
self._private.cursor_opacity = 1
self:get_text_widget():emit_signal("widget::redraw_needed")
end
function text_input:hide_cursor()
self._private.cursor_opacity = 0
self:get_text_widget():emit_signal("widget::redraw_needed")
end
function text_input:set_cursor_index(index)
index = math.max(math.min(index, #self:get_text()), 0)
local layout = self:get_text_widget()._private.layout
local strong_pos, weak_pos = layout:get_cursor_pos(index)
if strong_pos then
self._private.cursor_index = index
self._private.mode = "insert"
self._private.cursor_x = strong_pos.x / Pango.SCALE
self._private.cursor_y = strong_pos.y / Pango.SCALE
if self:get_focused() then
self:show_cursor()
end
self:hide_selection()
self:get_text_widget():emit_signal("widget::redraw_needed")
end
end
function text_input:set_cursor_index_from_x_y(x, y)
local layout = self:get_text_widget()._private.layout
local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
if index then
self:set_cursor_index(index)
else
self:set_cursor_index(#self:get_text())
end
end
function text_input:set_cursor_index_to_word_start()
self:set_cursor_index(cword_start(self:get_text(), self:get_cursor_index() + 1) - 1)
end
function text_input:set_cursor_index_to_word_end()
self:set_cursor_index(cword_end(self:get_text(), self:get_cursor_index() + 1) - 1)
end
function text_input:set_cursor_index_to_end()
self:set_cursor_index(#self:get_text())
end
function text_input:increamant_cursor_index()
if self:get_mode() == "insert" then
self:set_cursor_index(self:get_cursor_index() + 1)
else
local start_pos = self._private.selection_start
local end_pos = self._private.selection_end
if start_pos > end_pos then
start_pos, end_pos = end_pos, start_pos
end
self:set_cursor_index(end_pos)
end
end
function text_input:decremeant_cursor_index()
if self:get_mode() == "insert" then
self:set_cursor_index(self:get_cursor_index() - 1)
else
local start_pos = self._private.selection_start
local end_pos = self._private.selection_end
if start_pos > end_pos then
start_pos, end_pos = end_pos, start_pos
end
self:set_cursor_index(start_pos)
end
end
function text_input:get_cursor_index()
return self._private.cursor_index
end
function text_input:set_focus_on_subject_mouse_enter(subject)
subject:connect_signal("mouse::enter", function()
self:focus()
end)
end
function text_input:set_unfocus_on_subject_mouse_leave(subject)
subject:connect_signal("mouse::leave", function()
self:unfocus()
end)
end
function text_input:get_focused()
return self._private.focused
end
function text_input:focus()
local wp = self._private
if self:get_focused() == true then
return
end
self:show_cursor()
run_keygrabber(self)
if wp.unfocus_on_clicked_outside or wp.unfocus_on_clicked_inside then
run_mousegrabber(self)
end
if wp.cursor_blink then
gtimer.start_new(wp.cursor_blink_rate, function()
if self:get_focused() == true then
if self._private.cursor_opacity == 1 then
self:hide_cursor()
elseif self:get_mode() == "insert" then
self:show_cursor()
end
return true
end
return false
end)
end
wp.focused = true
self:emit_signal("focus")
capi.awesome.emit_signal("text_input::focus", self)
end
function text_input:unfocus()
local wp = self._private
if self:get_focused() == false then
return
end
self:hide_cursor()
self:hide_selection()
if self.reset_on_unfocus == true then
self:set_text("")
end
awful.keygrabber.stop(wp.keygrabber)
if wp.unfocus_on_clicked_outside then
capi.mousegrabber.stop()
end
capi.root.cursor("left_ptr")
local wibox = capi.mouse.current_wibox
if wibox then
wibox.cursor = "left_ptr"
end
wp.focused = false
self:emit_signal("unfocus")
capi.awesome.emit_signal("text_input::unfocus", self)
end
function text_input:toggle()
local wp = self._private
if self:get_focused() == false then
self:focus()
else
self:unfocus()
end
end
local function new()
local widget = wibox.container.background()
gtable.crush(widget, text_input, true)
local wp = widget._private
wp.focused = false
wp.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
wp.cursor_index = 0
wp.mode = "insert"
wp.cursor_x = 0
wp.cursor_y = 0
wp.cursor_opacity = 0
wp.selection_start_x = 0
wp.selection_end_x = 0
wp.selection_start_y = 0
wp.selection_end_y = 0
wp.selection_opacity = 0
wp.unfocus_keys = { "Escape", "Return" }
wp.unfocus_on_clicked_inside = false
wp.unfocus_on_clicked_outside = true
wp.unfocus_on_mouse_leave = false
wp.unfocus_on_tag_change = true
wp.unfocus_on_other_text_input_focus = true
wp.focus_on_subject_mouse_enter = nil
wp.unfocus_on_subject_mouse_leave = nil
wp.reset_on_unfocus = false
wp.placeholder = ""
wp.text = ""
wp.only_numbers = false
wp.round = false
wp.obscure = false
wp.cursor_width = 2
wp.cursor_bg = beautiful.fg_normal
wp.cursor_blink = true
wp.cursor_blink_rate = 0.6
wp.selection_bg = beautiful.bg_normal
widget:set_widget_template(wibox.widget {
layout = wibox.layout.stack,
{
widget = wibox.widget.textbox,
id = "placeholder_role",
text = wp.placeholder
},
{
widget = wibox.widget.textbox,
id = "text_role",
text = wp.text
}
})
capi.tag.connect_signal("property::selected", function()
if wp.unfocus_on_tag_change then
widget:unfocus()
end
end)
capi.awesome.connect_signal("text_input::focus", function(text_input)
if wp.unfocus_on_other_text_input_focus == false and text_input ~= self then
widget:unfocus()
end
end)
return widget
end
function text_input.mt:__call(...)
return new(...)
end
build_properties(text_input, properties)
return setmetatable(text_input, text_input.mt)