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)