diff --git a/widget/app_launcher/init.lua b/widget/app_launcher/init.lua
index 70fd630..9df9643 100644
--- a/widget/app_launcher/init.lua
+++ b/widget/app_launcher/init.lua
@@ -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, "Search: ")
- 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")
diff --git a/widget/app_launcher/prompt.lua b/widget/app_launcher/prompt.lua
deleted file mode 100644
index 15ac313..0000000
--- a/widget/app_launcher/prompt.lua
+++ /dev/null
@@ -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(
- '%s ',
- wp.icon.font, icon_size, wp.icon.color, wp.icon.icon)
- else
- local icon_size = dpi(ceil(wp.icon_size * 1024))
- markup = string.format(
- '%s ',
- 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(
- '%s' ..
- '%s' ..
- '%s' ..
- '%s%s',
- 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(
- '%s' ..
- '%s',
- 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)
diff --git a/widget/app_launcher/text_input.lua b/widget/app_launcher/text_input.lua
new file mode 100644
index 0000000..0974837
--- /dev/null
+++ b/widget/app_launcher/text_input.lua
@@ -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)