dotfiles/awes2/widget/calculator/init.lua

477 lines
11 KiB
Lua

----------------------------------------------------------------------------
--- Basic Calculator Widget
--
--
-- For more details check my repos README.md
--
--
-- @author manilarome <gerome.matilla07@gmail.com>
-- @copyright 2019 manilarome
-- @widget calculator
----------------------------------------------------------------------------
-- A basic calculator widget
-- Supports keyboard input!
-- Just hover your cursor above the calculator widget and start typing
-- Stop keygrabbing by leaving the calculator
local awful = require('awful')
local wibox = require('wibox')
local gears = require('gears')
local beautiful = require('beautiful')
local dpi = beautiful.xresources.apply_dpi
local clickable_container = require('widget.clickable-container')
local config_dir = gears.filesystem.get_configuration_dir()
local widget_icon_dir = config_dir .. 'widget/calculator/icons/'
local calculator_screen = wibox.widget {
{
id = 'calcu_screen',
text = '0',
font = 'SF Pro Text Regular 20',
align = 'right',
valign = 'center',
widget = wibox.widget.textbox,
},
margins = dpi(5),
widget = wibox.container.margin
}
-- Evaluate
local calculate = function ()
local calcu_screen = calculator_screen.calcu_screen
local string_expression = calcu_screen:get_text()
if string_expression:sub(-1):match("[%+%-%/%*%^%.]") then
return
end
local func = assert(load("return " .. string_expression))
local ans = tostring(func())
-- Convert -nan to undefined
if ans == '-nan' then
calcu_screen:set_text('undefined')
return
end
-- Set the answer in textbox
calcu_screen:set_text(ans)
end
local txt_on_screen = function()
screen_text = calculator_screen.calcu_screen:get_text()
return screen_text == 'inf' or screen_text == 'undefined' or screen_text == 'SYNTAX ERROR' or #screen_text == 1
end
-- Delete the last digit in screen
local delete_value = function()
calcu_screen = calculator_screen.calcu_screen
-- Set the screen text to 0 if conditions met
if txt_on_screen() then
calcu_screen:set_text('0')
else
-- Delete the last digit
calcu_screen:set_text(calcu_screen:get_text():sub(1, -2))
end
end
-- Clear screen
local clear_screen = function()
calculator_screen.calcu_screen:set_text('0')
end
-- The one that filters and checks the user input to avoid errors and bugs
local format_screen = function(value)
local calcu_screen = calculator_screen.calcu_screen
-- If the screen has only 0
if calcu_screen:get_text() == '0' then
-- Check if the button pressed sends a value of either +, -, /, *, ^, .
if value:sub(-1):match("[%+%/%*%^%.]") then
calcu_screen:set_text(calcu_screen:get_text() .. tostring(value))
else
calcu_screen:set_text(value)
end
elseif calcu_screen:get_text() == 'inf' or
calcu_screen:get_text() == 'undefined' or
calcu_screen:get_text() == 'SYNTAX ERROR' then
-- Clear screen if an operator is selected
if value:sub(-1):match("[%+%/%*%^%.]") then
clear_screen()
else
-- Replace screen txt with the number value pressed
clear_screen()
calcu_screen:set_text(tostring(value))
end
else
-- Don't let the user to input two or more consecutive arithmetic operators and decimals
if calcu_screen:get_text():sub(-1):match("[%+%-%/%*%^%.]") and value:sub(-1):match("[%+%-%/%*%^%.%%]") then
-- Get the operator from button pressed
local string_eval = calcu_screen:get_text():sub(-1):gsub("[%+%-%/%*%^%.]", value)
-- This will prevent the user to input consecutive operators and decimals
-- It will replace the previous operator with the value of input
calcu_screen:set_text(calcu_screen:get_text():sub(1, -2))
-- Concatenate the value operator to the screen string to replace the deleted operator
calcu_screen:set_text(calcu_screen:get_text() .. tostring(string_eval))
else
-- Concatenate the value to screen string
calcu_screen:set_text(calcu_screen:get_text() .. tostring(value))
end
end
end
-- Shape generator
local build_shape = function (position, radius)
-- Position represents the position of rounded corners
if position == 'top' then
return function(cr, width, height)
gears.shape.partially_rounded_rect(cr, width, height, true, true, false, false, radius)
end
elseif position == 'top_left' then
return function(cr, width, height)
gears.shape.partially_rounded_rect(cr, width, height, true, false, false, false, radius)
end
elseif position == 'top_right' then
return function(cr, width, height)
gears.shape.partially_rounded_rect(cr, width, height, false, true, false, false, radius)
end
elseif position == 'bottom_right' then
return function(cr, width, height)
gears.shape.partially_rounded_rect(cr, width, height, false, false, true, false, radius)
end
elseif position == 'bottom_left' then
return function(cr, width, height)
gears.shape.partially_rounded_rect(cr, width, height, false, false, false, true, radius)
end
else
return function(cr, width, height)
gears.shape.rounded_rect(cr, width, height, radius)
end
end
end
-- Themes widgets
local decorate_widget = function(widget_arg, pos, rad)
return wibox.widget {
widget_arg,
bg = beautiful.groups_bg,
shape = build_shape(pos, rad),
widget = wibox.container.background
}
end
-- Build a button
local build_button_widget = function(text, rcp, rad)
local value = text
local build_textbox = wibox.widget {
{
id = 'btn_name',
text = value,
font = 'SF Pro Text 12',
align = 'center',
valign = 'center',
widget = wibox.widget.textbox,
},
margins = dpi(5),
widget = wibox.container.margin
}
local build_button = wibox.widget {
{
build_textbox,
margins = dpi(7),
widget = wibox.container.margin
},
widget = clickable_container
}
build_button:buttons(
gears.table.join(
awful.button(
{},
1,
nil,
function ()
if value == 'C' then
clear_screen()
elseif value == '=' then
-- Calculate and error handling
if not pcall(calculate) then
calculator_screen.calcu_screen:set_text('SYNTAX ERROR')
end
elseif value == 'DEL' then
delete_value()
else
format_screen(value)
end
end
)
)
)
return decorate_widget(build_button, rcp, rad)
end
local keygrab_running = false
local kb_imagebox = wibox.widget {
id = 'kb_icon',
image = widget_icon_dir .. 'kb-off' .. '.svg',
resize = true,
forced_height = dpi(15),
widget = wibox.widget.imagebox
}
local kb_button_widget = wibox.widget {
{
{
{
layout = wibox.layout.align.horizontal,
expand = 'none',
nil,
kb_imagebox,
nil
},
margins = dpi(10),
widget = wibox.container.margin
},
widget = clickable_container
},
bg = beautiful.groups_bg,
shape = build_shape('bottom_left', beautiful.groups_radius),
widget = wibox.container.background
}
local toggle_btn_keygrab = function()
if keygrab_running then
kb_imagebox:set_image(widget_icon_dir .. 'kb-off' .. '.svg')
awesome.emit_signal("widget::calc_stop_keygrab")
keygrab_running = false
else
kb_imagebox:set_image(widget_icon_dir .. 'kb' .. '.svg')
awesome.emit_signal("widget::calc_start_keygrab")
keygrab_running = true
end
end
local kb_button = kb_button_widget
kb_button:buttons(
gears.table.join(
awful.button(
{},
1,
nil,
function()
toggle_btn_keygrab()
end
)
)
)
local calcu_keygrabber = awful.keygrabber {
auto_start = true,
stop_event = 'release',
start_callback = function()
keygrab_running = true
kb_imagebox:set_image(widget_icon_dir .. 'kb' .. '.svg')
end,
stop_callback = function()
keygrab_running = false
kb_imagebox:set_image(widget_icon_dir .. 'kb-off' .. '.svg')
end,
keypressed_callback = function(self, mod, key, command)
if #key == 1 and (key:match('%d+') or key:match('[%+%-%/%*%^%.]')) then
format_screen(key)
elseif key == 'BackSpace' then
delete_value()
elseif key == 'Escape' then
clear_screen()
elseif key == 'x' then
awesome.emit_signal("widget::calc_stop_keygrab")
elseif key == '=' or key == 'Return' then
-- Calculate
if not pcall(calculate) then
calculator_screen.calcu_screen:set_text('SYNTAX ERROR')
end
end
end,
}
local calculator_body = wibox.widget {
layout = wibox.layout.fixed.vertical,
spacing = dpi(1),
{
spacing = dpi(1),
layout = wibox.layout.flex.horizontal,
decorate_widget(calculator_screen, 'top', beautiful.groups_radius),
},
{
spacing = dpi(1),
layout = wibox.layout.flex.horizontal,
build_button_widget('C', 'flat' , 0),
build_button_widget('^', 'flat', 0),
build_button_widget('/', 'flat', 0),
build_button_widget('DEL', 'flat', 0),
},
{
spacing = dpi(1),
layout = wibox.layout.flex.horizontal,
build_button_widget('7', 'flat', 0),
build_button_widget('8', 'flat', 0),
build_button_widget('9', 'flat', 0),
build_button_widget('*', 'flat', 0),
},
{
spacing = dpi(1),
layout = wibox.layout.flex.horizontal,
build_button_widget('4', 'flat', 0),
build_button_widget('5', 'flat', 0),
build_button_widget('6', 'flat', 0),
build_button_widget('-', 'flat', 0),
},
{
spacing = dpi(1),
layout = wibox.layout.flex.horizontal,
build_button_widget('1', 'flat', 0),
build_button_widget('2', 'flat', 0),
build_button_widget('3', 'flat', 0),
build_button_widget('+', 'flat', 0),
},
{
spacing = dpi(1),
layout = wibox.layout.flex.horizontal,
kb_button,
build_button_widget('0', 'flat', 0),
build_button_widget('.', 'flat', 0),
build_button_widget('=', 'bottom_right', beautiful.groups_radius)
},
}
calculator_body:connect_signal(
"mouse::enter",
function()
-- Start keygrabbing
calcu_keygrabber:start()
end
)
calculator_body:connect_signal(
"mouse::leave",
function()
-- Stop keygrabbing
calcu_keygrabber:stop()
end
)
awesome.connect_signal(
"widget::calc_start_keygrab",
function()
-- Stop keygrabbing
calcu_keygrabber:start()
end
)
awesome.connect_signal(
"widget::calc_stop_keygrab",
function()
-- Stop keygrabbing
calcu_keygrabber:stop()
end
)
local calcu_tooltip = awful.tooltip {
objects = {kb_button},
mode = 'outside',
align = 'right',
delay_show = 1,
preferred_positions = {'right', 'left', 'top', 'bottom'},
margin_leftright = dpi(8),
margin_topbottom = dpi(8),
markup = [[
<b>Tips:</b>
Enable keyboard support by hovering your mouse above the calculator.
Or toggle it on/off by pressing the keyboard button.
Only numbers, arithmetic operators, and decimal point is accepted.
<b>Keyboard bindings:</b>
<b>=</b> and <b>Return</b> to get the answer.
<b>BackSpace</b> to delete the last digit.
<b>Escape</b> clears the screen.
<b>x</b> stops keygrabbing.
<b>Note:</b>
While in keygrabbing mode, your keyboard's focus will be on the calculator.
So you're AwesomeWM keybinding will stop working.
<b>Stopping the keygrabbing mode:</b>
* Move away your cursor from the calculator.
* Toggle it off using the keyboard button.
* Press <b>x</b>.
]]
}
return calculator_body