--------------------------------------------------------------------------- -- 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) 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) args = args or {} -- 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)