--------------------------------------------------------------------------- -- This widget can be used to type text and get the text from it. --@DOC_wibox_widget_defaults_inputbox_EXAMPLE@ -- -- @author Rene Kievits -- @copyright 2022, Rene Kievits -- @module awful.widget.inputbox --------------------------------------------------------------------------- local setmetatable = setmetatable local beautiful = require("beautiful") local gtable = require("gears.table") local base = require("wibox.widget.base") local gstring = require("gears.string") local akeygrabber = require("awful.keygrabber") local akey = require("awful.key") local textbox = require("wibox.widget.textbox") local capi = { selection = selection, mousegrabber = mousegrabber, mouse = mouse, } local inputbox = { mt = {} } --- Formats the text with a cursor and highlights if set. local function text_with_cursor(text, cursor_pos, self) local char, spacer, text_start, text_end local cursor_fg = beautiful.inputbox_cursor_fg or "#313131" local cursor_bg = beautiful.inputbox_cursor_bg or "#0dccfc" local placeholder_text = self.hint_text or "" local placeholder_fg = beautiful.inputbox_placeholder_fg or "#777777" local highlight_bg = beautiful.inputbox_highlight_bg or "#35ffe4" local highlight_fg = beautiful.inputbox_highlight_fg or "#000000" if text == "" then return "" .. placeholder_text .. "" end local offset = 0 if text:sub(cursor_pos - 1, cursor_pos - 1) == -1 then offset = 1 end if #text < cursor_pos then char = " " spacer = "" text_start = gstring.xml_escape(text) text_end = "" else char = gstring.xml_escape(text:sub(cursor_pos, cursor_pos + offset)) spacer = " " text_start = gstring.xml_escape(text:sub(1, cursor_pos - 1)) text_end = gstring.xml_escape(text:sub(cursor_pos + offset + 1)) end if self._private.highlight and self._private.highlight.cur_pos_start and self._private.highlight.cur_pos_end then -- split the text into 3 parts based on the highlight and cursor position local text_start_highlight = gstring.xml_escape(text:sub(1, self._private.highlight.cur_pos_start - 1)) local text_highlighted = gstring.xml_escape(text:sub(self._private.highlight.cur_pos_start, self._private.highlight.cur_pos_end)) local text_end_highlight = gstring.xml_escape(text:sub(self._private.highlight.cur_pos_end + 1)) return text_start_highlight .. "" .. text_highlighted .. "" .. text_end_highlight else return text_start .. "" .. char .. "" .. text_end .. spacer end end function inputbox:layout(_, width, height) if self._private.widget then return { base.place_widget_at(self._private.widget, 0, 0, width, height) } end end function inputbox:fit(context, width, height) local w, h = 0, 0 if self._private.widget then w, h = base.fit_widget(self, context, self._private.widget, width, height) end return w, h end inputbox.set_widget = base.set_widget_common --- Clears the current text function inputbox:clear() self:set_text("") end function inputbox:get_text() return self._private.text or "" end function inputbox:set_text(text) self._private.text = text self.markup = text_with_cursor(self:get_text(), #self:get_text(), self) self:emit_signal("property::text", text) end --- Stop the keygrabber and mousegrabber function inputbox:stop() if (not self.akeygrabber) or (not self.akeygrabber.is_running) then return end self:emit_signal("stopped") self.akeygrabber.stop() end function inputbox:focus() if (not self.akeygrabber) or (not self.akeygrabber.is_running) then akeygrabber.stop() self:run() end self:connect_signal("button::press", function() if capi.mouse.current_widget ~= self then self:emit_signal("keygrabber::stop", "") end end) end --- Init the inputbox and start the keygrabber function inputbox:run() if not self._private.text then self._private.text = "" end -- Init the cursor position, but causes on refocus the cursor to move to the left local cursor_pos = #self:get_text() + 1 -- Init and reset(when refocused) the highlight self._private.highlight = {} self.akeygrabber = akeygrabber { autostart = true, start_callback = function() self:emit_signal("started") end, stop_callback = function(_, stop_key) if stop_key == "Return" then self:emit_signal("submit", self:get_text(), stop_key) -- Only reset text on enter as on escape you might want to continue later self:set_text("") else self:emit_signal("stopped", stop_key) end end, stop_key = { "Escape", "Return" }, keybindings = { --lShift, rShift = #50, #62 --lControl, rControl = #37, #105 akey { modifiers = { "Shift" }, key = "Left", -- left on_press = function() if cursor_pos > 1 then local offset = (self._private.text:sub(cursor_pos - 1, cursor_pos - 1):wlen() == -1) and 1 or 0 if not self._private.highlight.cur_pos_start then self._private.highlight.cur_pos_start = cursor_pos - 1 end if not self._private.highlight.cur_pos_end then self._private.highlight.cur_pos_end = cursor_pos end if self._private.highlight.cur_pos_start < cursor_pos then self._private.highlight.cur_pos_end = self._private.highlight.cur_pos_end - 1 else self._private.highlight.cur_pos_start = self._private.highlight.cur_pos_start end cursor_pos = cursor_pos - 1 end if cursor_pos < 1 then cursor_pos = 1 elseif cursor_pos > #self._private.text + 1 then cursor_pos = #self._private.text + 1 end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", "Shift", "Left") end }, akey { modifiers = { "Shift" }, key = "Right", -- right on_press = function() if #self._private.text >= cursor_pos then if not self._private.highlight.cur_pos_end then self._private.highlight.cur_pos_end = cursor_pos - 1 end if not self._private.highlight.cur_pos_start then self._private.highlight.cur_pos_start = cursor_pos end if self._private.highlight.cur_pos_end <= cursor_pos then self._private.highlight.cur_pos_end = self._private.highlight.cur_pos_end + 1 else self._private.highlight.cur_pos_start = self._private.highlight.cur_pos_start + 1 end cursor_pos = cursor_pos + 1 if cursor_pos > #self._private.text + 1 then self._private.highlight = {} end end if cursor_pos < 1 then cursor_pos = 1 elseif cursor_pos > #self._private.text + 1 then cursor_pos = #self._private.text + 1 end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", "Shift", "Right") end }, akey { modifiers = { "Control" }, key = "a", -- a on_press = function() -- Mark the entire text self._private.highlight = { cur_pos_start = 1, cur_pos_end = #self._private.text } self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", "Control", "a") end }, akey { modifiers = { "Control" }, key = "v", -- v on_press = function() local sel = capi.selection() if sel then sel = sel:gsub("\n", "") if self._private.highlight and self._private.highlight.cur_pos_start and self._private.highlight.cur_pos_end then -- insert the text into the selected part local text_start = self._private.text:sub(1, self._private.highlight.cur_pos_start - 1) local text_end = self._private.text:sub(self._private.highlight.cur_pos_end + 1) self:set_text(text_start .. sel .. text_end) self._private.highlight = {} cursor_pos = #text_start + #sel + 1 else self:set_text(self._private.text:sub(1, cursor_pos - 1) .. sel .. self._private.text:sub(cursor_pos)) cursor_pos = cursor_pos + #sel end end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", "Control", "v") end }, akey { modifiers = { "Control" }, key = "c", -- c on_press = function() --TODO end }, akey { modifiers = { "Control" }, key = "x", -- x on_press = function() --TODO end }, akey { modifiers = { "Control" }, key = "Left", -- left on_press = function() -- Find all spaces local spaces = {} local t, i = self._private.text, 0 while t:find("%s") do i = t:find("%s") table.insert(spaces, i) t = t:sub(1, i - 1) .. "-" .. t:sub(i + 1) end local cp = 1 for _, v in ipairs(spaces) do if (v < cursor_pos) then cp = v end end cursor_pos = cp if cursor_pos < 1 then cursor_pos = 1 elseif cursor_pos > #self._private.text + 1 then cursor_pos = #self._private.text + 1 end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", "Control", "Left") end }, akey { modifiers = { "Control" }, key = "Right", -- right on_press = function() local next_space = self._private.text:sub(cursor_pos):find("%s") if next_space then cursor_pos = cursor_pos + next_space else cursor_pos = #self._private.text + 1 end if cursor_pos < 1 then cursor_pos = 1 elseif cursor_pos > #self._private.text + 1 then cursor_pos = #self._private.text + 1 end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", "Control", "Right") end }, akey { modifiers = {}, key = "BackSpace", --BackSpace on_press = function() -- If text is highlighted delete that, else just delete the character to the left if self._private.highlight and self._private.highlight.cur_pos_start and self._private.highlight.cur_pos_end then local text_start = self._private.text:sub(1, self._private.highlight.cur_pos_start - 1) local text_end = self._private.text:sub(self._private.highlight.cur_pos_end + 1) self:set_text(text_start .. text_end) self._private.highlight = {} cursor_pos = #text_start + 1 else if cursor_pos > 1 then local offset = (self._private.text:sub(cursor_pos - 1, cursor_pos - 1):wlen() == -1) and 1 or 0 self:set_text(self._private.text:sub(1, cursor_pos - 2 - offset) .. self._private.text:sub(cursor_pos)) cursor_pos = cursor_pos - 1 - offset end end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", nil, "BackSpace") end }, akey { modifiers = {}, key = "Delete", --delete on_press = function() -- If text is highlighted delete that, else just delete the character to the right if self._private.highlight and self._private.highlight.cur_pos_start and self._private.highlight.cur_pos_end then local text_start = self._private.text:sub(1, self._private.highlight.cur_pos_start - 1) local text_end = self._private.text:sub(self._private.highlight.cur_pos_end + 1) self:set_text(text_start .. text_end) self._private.highlight = {} cursor_pos = #text_start + 1 else if cursor_pos <= #self._private.text then self:set_text(self._private.text:sub(1, cursor_pos - 1) .. self._private.text:sub(cursor_pos + 1)) end end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", nil, "Delete") end }, akey { modifiers = {}, key = "Left", --left on_press = function() -- Move cursor ro the left if cursor_pos > 1 then cursor_pos = cursor_pos - 1 end self._private.highlight = {} end }, akey { modifiers = {}, key = "Right", --right on_press = function() -- Move cursor to the right if cursor_pos <= #self._private.text then cursor_pos = cursor_pos + 1 end self._private.highlight = {} end }, --self.keybindings }, keypressed_callback = function(_, modifiers, key) if modifiers[1] == "Shift" then if key:wlen() == 1 then self:set_text(self._private.text:sub(1, cursor_pos - 1) .. string.upper(key) .. self._private.text:sub(cursor_pos)) cursor_pos = cursor_pos + #key end elseif modifiers[1] == "Mod2" or "" then if key:wlen() == 1 then self:set_text(self._private.text:sub(1, cursor_pos - 1) .. key .. self._private.text:sub(cursor_pos)) cursor_pos = cursor_pos + #key end end if cursor_pos < 1 then cursor_pos = 1 elseif cursor_pos > #self._private.text + 1 then cursor_pos = #self._private.text + 1 end self.markup = text_with_cursor(self:get_text(), cursor_pos, self) self:emit_signal("inputbox::key_pressed", modifiers, key) end } end --- Creates a new inputbox widget -- @tparam table args Arguments for the inputbox widget -- @tparam string args.text The text to display in the inputbox -- @tparam[opt=beautiful.fg_normal] string args.fg Text foreground color -- @tparam[opt=beautiful.border_focus] string args.border_focus_color Border color when focused -- @tparam[opt=""] string args.placeholder_text placeholder text to be shown when not focused and -- @tparam[opt=beautiful.inputbox_placeholder_fg] string args.placeholder_fg placeholder text foreground color -- @tparam[opt=beautiful.inputbox_cursor_bg] string args.cursor_bg Cursor background color -- @tparam[opt=beautiful.inputbox_cursor_fg] string args.cursor_fg Cursor foreground color -- @tparam[opt=beautiful.inputbox_highlight_bg] string args.highlight_bg Highlight background color -- @tparam[opt=beautiful.inputbox_highlight_fg] string args.highlight_fg Highlight foreground color -- @treturn awful.widget.inputbox The inputbox widget. -- @constructorfct awful.widget.inputbox function inputbox.new(args) -- directly pass a possible default text(this is not meant to be a hint) local w = textbox() --gtable.crush(w, args) gtable.crush(w, inputbox, true) w.font = args.font or beautiful.font w.keybindings = args.keybindings or {} w.hint_text = args.hint_text w.markup = args.text or text_with_cursor("", 1, w) return w end function inputbox.mt:__call(...) return inputbox.new(...) end return setmetatable(inputbox, inputbox.mt)