477 lines
11 KiB
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
|