layout-machi/editor.lua

723 lines
22 KiB
Lua
Raw Normal View History

2019-07-07 22:19:18 +02:00
local machi = {
2019-07-07 22:43:54 +02:00
layout = require((...):match("(.-)[^%.]+$") .. "layout"),
2019-07-07 22:19:18 +02:00
}
2019-07-04 23:32:05 +02:00
local api = {
beautiful = require("beautiful"),
wibox = require("wibox"),
awful = require("awful"),
screen = require("awful.screen"),
layout = require("awful.layout"),
keygrabber = require("awful.keygrabber"),
naughty = require("naughty"),
gears = require("gears"),
2019-07-06 23:06:14 +02:00
lgi = require("lgi"),
2019-07-04 23:32:05 +02:00
dpi = require("beautiful.xresources").apply_dpi,
}
2019-07-06 23:06:14 +02:00
local function with_alpha(col, alpha)
_, r, g, b, a = col:get_rgba()
return api.lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
end
2019-07-07 22:43:54 +02:00
local function max(a, b)
if a < b then return b else return a end
end
2019-07-04 23:32:05 +02:00
local label_font_family = api.beautiful.get_font(
api.beautiful.mono_font or api.beautiful.font):get_family()
local label_size = api.dpi(30)
local info_size = api.dpi(60)
-- colors are in rgba
2019-07-06 23:06:14 +02:00
local border_color = with_alpha(api.gears.color(api.beautiful.border_focus), 0.75)
local active_color = with_alpha(api.gears.color(api.beautiful.bg_focus), 0.5)
local open_color = with_alpha(api.gears.color(api.beautiful.bg_normal), 0.5)
local closed_color = open_color
2019-07-04 23:32:05 +02:00
local init_max_depth = 2
2019-07-06 06:10:04 +02:00
local function is_tiling(c)
2019-07-04 23:32:05 +02:00
return
not (c.tomb_floating or c.floating or c.maximized_horizontal or c.maximized_vertical or c.maximized or c.fullscreen)
end
2019-07-06 06:10:04 +02:00
local function set_tiling(c)
2019-07-04 23:32:05 +02:00
c.floating = false
c.maximized = false
c.maximized_vertical = false
c.maximized_horizontal = false
c.fullscreen = false
end
2019-07-06 06:10:04 +02:00
local function min(a, b)
2019-07-04 23:32:05 +02:00
if a < b then return a else return b end
end
2019-07-06 06:10:04 +02:00
local function max(a, b)
2019-07-04 23:32:05 +02:00
if a < b then return b else return a end
end
2019-07-06 06:10:04 +02:00
local function set_region(c, r)
2019-07-04 23:32:05 +02:00
c.floating = false
c.maximized = false
c.fullscreen = false
c.machi_region = r
api.layout.arrange(c.screen)
end
2019-07-06 06:35:41 +02:00
--- fit the client into the machi of the screen
-- @param c the client to fit
-- @param cycle whether to cycle the region if the window is already in machi
-- @return whether any actions have been taken on the client
2019-07-06 06:16:58 +02:00
local function fit_region(c, cycle)
2019-07-04 23:32:05 +02:00
layout = api.layout.get(c.screen)
regions = layout.get_regions and layout.get_regions()
if type(regions) ~= "table" or #regions < 1 then
2019-07-06 06:35:41 +02:00
return false
2019-07-04 23:32:05 +02:00
end
current_region = c.machi_region or 1
if not is_tiling(c) then
-- find out which region has the most intersection, calculated by a cap b / a cup b
2019-07-07 22:19:18 +02:00
c.machi_region = machi.layout.find_region(c, regions)
2019-07-04 23:32:05 +02:00
set_tiling(c)
2019-07-06 06:16:58 +02:00
elseif cycle then
if current_region >= #regions then
c.machi_region = 1
else
c.machi_region = current_region + 1
end
api.layout.arrange(c.screen)
2019-07-06 06:35:41 +02:00
else
return false
2019-07-04 23:32:05 +02:00
end
2019-07-06 06:35:41 +02:00
return true
2019-07-04 23:32:05 +02:00
end
2019-07-06 06:10:04 +02:00
local function _area_tostring(wa)
2019-07-04 23:32:05 +02:00
return "{x:" .. tostring(wa.x) .. ",y:" .. tostring(wa.y) .. ",w:" .. tostring(wa.width) .. ",h:" .. tostring(wa.height) .. "}"
end
2019-07-06 06:10:04 +02:00
local function shrink_area_with_gap(a, gap)
2019-07-04 23:32:05 +02:00
return { x = a.x + (a.bl and 0 or gap / 2), y = a.y + (a.bu and 0 or gap / 2),
width = a.width - (a.bl and 0 or gap / 2) - (a.br and 0 or gap / 2),
height = a.height - (a.bu and 0 or gap / 2) - (a.bd and 0 or gap / 2) }
end
2019-07-06 06:10:04 +02:00
local function restore_data(data)
if data.history_file then
local file, err = io.open(data.history_file, "r")
if err then
print("cannot read history from " .. data.history_file)
else
data.cmds = {}
2019-07-08 22:58:09 +02:00
data.last_cmd = {}
local last_layout_name
2019-07-06 06:10:04 +02:00
for line in file:lines() do
2019-07-08 22:58:09 +02:00
if line:sub(1, 1) == "+" then
last_layout_name = line:sub(2, #line)
else
if last_layout_name ~= nil then
print("restore last cmd " .. line .. " for " .. last_layout_name)
data.last_cmd[last_layout_name] = line
last_layout_name = nil
else
print("restore cmd " .. line)
data.cmds[#data.cmds + 1] = line
end
end
2019-07-06 06:10:04 +02:00
end
2019-07-09 21:04:20 +02:00
file:close()
2019-07-06 06:10:04 +02:00
end
end
return data
end
local function create(data)
if data == nil then
data = restore_data({
history_file = ".machi_history",
2019-07-06 06:11:17 +02:00
history_save_max = 100,
2019-07-06 06:11:41 +02:00
gap = api.beautiful.useless_gap,
2019-07-06 06:10:04 +02:00
})
end
2019-07-08 06:14:23 +02:00
if data.cmds == nil then
data.cmds = {}
end
2019-07-08 22:58:09 +02:00
if data.last_cmd == nil then
data.last_cmd = {}
end
2019-07-05 00:47:29 +02:00
local gap = data.gap or 0
2019-07-04 23:32:05 +02:00
local closed_areas
local open_areas
local history
2019-07-10 04:07:54 +02:00
local args
2019-07-04 23:32:05 +02:00
local max_depth
local current_info
local current_cmd
local to_exit
local to_apply
local function init(init_area)
2019-07-04 23:32:05 +02:00
closed_areas = {}
open_areas = {
{
x = init_area.x,
y = init_area.y,
width = init_area.width,
height = init_area.height,
border = 15,
depth = 0,
group_id = 0,
-- we do not want to rely on BitOp
bl = true, br = true, bu = true, bd = true,
}
}
2019-07-04 23:32:05 +02:00
history = {}
2019-07-10 04:07:54 +02:00
args = ""
2019-07-04 23:32:05 +02:00
max_depth = init_max_depth
current_info = ""
current_cmd = ""
to_exit = false
to_apply = false
end
local function push_history()
2019-07-10 04:07:54 +02:00
history[#history + 1] = {#closed_areas, #open_areas, {}, current_info, current_cmd, max_depth, args}
2019-07-04 23:32:05 +02:00
end
local function discard_history()
table.remove(history, #history)
end
local function pop_history()
if #history == 0 then return end
for i = history[#history][1] + 1, #closed_areas do
table.remove(closed_areas, #closed_areas)
end
for i = history[#history][2] + 1, #open_areas do
table.remove(open_areas, #open_areas)
end
for i = 1, #history[#history][3] do
open_areas[history[#history][2] - i + 1] = history[#history][3][i]
end
current_info = history[#history][4]
current_cmd = history[#history][5]
max_depth = history[#history][6]
2019-07-10 04:07:54 +02:00
args = history[#history][7]
2019-07-04 23:32:05 +02:00
table.remove(history, #history)
end
local function pop_open_area()
local a = open_areas[#open_areas]
table.remove(open_areas, #open_areas)
local idx = history[#history][2] - #open_areas
-- only save when the position has been firstly poped
if idx > #history[#history][3] then
history[#history][3][#history[#history][3] + 1] = a
end
return a
end
2019-07-05 16:26:37 +02:00
local split_count = 0
2019-07-04 23:32:05 +02:00
local function handle_split(method, alt)
2019-07-05 16:26:37 +02:00
split_count = split_count + 1
2019-07-04 23:32:05 +02:00
local a = pop_open_area()
2019-07-10 04:07:54 +02:00
print("split " .. method .. " " .. tostring(alt) .. " " .. args .. " " .. _area_tostring(a))
2019-07-04 23:32:05 +02:00
if method == "h" then
2019-07-10 04:07:54 +02:00
if #args == 0 then
args = "11"
elseif #args == 1 then
args = args .. "1"
end
local total = 0
local offset = {}
for i = 1, #args do
local arg
if not alt then
arg = tonumber(args:sub(i, i))
else
arg = tonumber(args:sub(#args - i + 1, #args - i + 1))
end
if arg < 1 then arg = 1 end
offset[#offset + 1] = total
total = total + arg
end
offset[#offset + 1] = total
local children = {}
for i = 1, #offset - 1 do
local child = {
x = a.x + a.width / total * offset[i],
y = a.y,
width = a.width / total * (offset[i + 1] - offset[i]),
height = a.height,
depth = a.depth + 1,
group_id = split_count,
bl = i == 1 and a.bl or false,
br = i == #offset and a.br or false,
bu = a.bu,
bd = a.bd,
}
children[#children + 1] = child
end
for i = #children, 1, -1 do
open_areas[#open_areas + 1] = children[i]
end
2019-07-04 23:32:05 +02:00
elseif method == "v" then
2019-07-10 04:07:54 +02:00
if #args == 0 then
args = "11"
elseif #args == 1 then
args = args .. "1"
end
local total = 0
local offset = {}
for i = 1, #args do
local arg
if not alt then
arg = tonumber(args:sub(i, i))
else
arg = tonumber(args:sub(#args - i + 1, #args - i + 1))
end
if arg < 1 then arg = 1 end
offset[#offset + 1] = total
total = total + arg
end
offset[#offset + 1] = total
local children = {}
for i = 1, #offset - 1 do
local child = {
x = a.x,
y = a.y + a.height / total * offset[i],
width = a.width,
height = a.height / total * (offset[i + 1] - offset[i]),
depth = a.depth + 1,
group_id = split_count,
bl = a.bl,
br = a.br,
bu = i == 1 and a.bu or false,
bd = i == #offset and a.bd or false,
}
children[#children + 1] = child
end
for i = #children, 1, -1 do
open_areas[#open_areas + 1] = children[i]
end
2019-07-04 23:32:05 +02:00
elseif method == "w" then
2019-07-10 04:07:54 +02:00
if #args == 0 then
args = "11"
elseif #args == 1 then
args = "1" .. args
end
if alt then args = string.reverse(args) end
local h_split = tonumber(args:sub(#args - 1, #args - 1))
local v_split = tonumber(args:sub(#args, #args))
if h_split < 1 then h_split = 1 end
if v_split < 1 then v_split = 1 end
local x_interval = a.width / h_split
local y_interval = a.height / v_split
for y = v_split, 1, -1 do
for x = h_split, 1, -1 do
2019-07-04 23:32:05 +02:00
local r = {
x = a.x + x_interval * (x - 1),
y = a.y + y_interval * (y - 1),
width = x_interval,
height = y_interval,
2019-07-05 16:26:37 +02:00
depth = a.depth + 1,
group_id = split_count,
2019-07-04 23:32:05 +02:00
}
if x == 1 then r.bl = a.bl else r.bl = false end
2019-07-10 04:07:54 +02:00
if x == h_split then r.br = a.br else r.br = false end
2019-07-04 23:32:05 +02:00
if y == 1 then r.bu = a.bu else r.bu = false end
2019-07-10 04:07:54 +02:00
if y == v_split then r.bd = a.bd else r.bd = false end
2019-07-04 23:32:05 +02:00
open_areas[#open_areas + 1] = r
end
end
2019-07-10 04:07:54 +02:00
elseif method == "p" then
2019-07-04 23:32:05 +02:00
-- XXX
end
2019-07-10 04:07:54 +02:00
args = ""
2019-07-04 23:32:05 +02:00
end
local function push_area()
closed_areas[#closed_areas + 1] = pop_open_area()
end
local function handle_command(key)
if key == "h" or key == "H" then
handle_split("h", key == "H")
elseif key == "v" or key == "V" then
handle_split("v", key == "V")
elseif key == "w" or key == "W" then
2019-07-10 04:07:54 +02:00
if args == "" then
2019-07-04 23:32:05 +02:00
push_area()
else
handle_split("w", key == "W")
end
elseif key == "p" or key == "P" then
handle_split("p", key == "P")
elseif key == "s" or key == "S" then
if #open_areas > 0 then
key = "s"
2019-07-10 04:07:54 +02:00
local times = args == "" and 1 or tonumber(args)
2019-07-05 16:26:37 +02:00
local t = {}
while #open_areas > 0 do
2019-07-04 23:32:05 +02:00
t[#t + 1] = pop_open_area()
end
for i = #t, 1, -1 do
2019-07-05 05:35:59 +02:00
open_areas[#open_areas + 1] = t[(i + times - 1) % #t + 1]
2019-07-04 23:32:05 +02:00
end
2019-07-10 04:07:54 +02:00
args = ""
2019-07-04 23:32:05 +02:00
else
return nil
end
elseif key == " " or key == "-" then
key = "-"
2019-07-10 04:07:54 +02:00
if args == "" then
2019-07-04 23:32:05 +02:00
push_area()
2019-07-10 04:07:54 +02:00
else
max_depth = tonumber(args)
args = ""
2019-07-04 23:32:05 +02:00
end
elseif key == "Return" or key == "." then
key = "."
while #open_areas > 0 do
push_area()
end
2019-07-10 04:07:54 +02:00
args = ""
2019-07-04 23:32:05 +02:00
elseif tonumber(key) ~= nil then
2019-07-10 04:07:54 +02:00
args = args .. key
2019-07-04 23:32:05 +02:00
else
return nil
end
while #open_areas > 0 and open_areas[#open_areas].depth >= max_depth do
push_area()
end
return key
end
local function start_interactive()
local cmd_index = #data.cmds + 1
data.cmds[cmd_index] = ""
local screen = api.screen.focused()
2019-07-08 19:15:05 +02:00
local screen_x = screen.geometry.x
local screen_y = screen.geometry.y
local kg
local infobox = api.wibox({
2019-07-06 18:22:19 +02:00
screen = screen,
x = screen.workarea.x,
y = screen.workarea.y,
width = screen.workarea.width,
height = screen.workarea.height,
bg = "#ffffff00",
opacity = 1,
ontop = true
})
infobox.visible = true
local function cleanup()
infobox.visible = false
end
2019-07-04 23:32:05 +02:00
local function draw_info(context, cr, width, height)
cr:set_source_rgba(0, 0, 0, 0)
cr:rectangle(0, 0, width, height)
cr:fill()
2019-07-04 23:32:05 +02:00
local msg, ext
for i, a in ipairs(closed_areas) do
local sa = shrink_area_with_gap(a, gap)
2019-07-08 19:15:05 +02:00
cr:rectangle(sa.x - screen_x, sa.y - screen_y, sa.width, sa.height)
cr:clip()
2019-07-06 23:06:14 +02:00
cr:set_source(closed_color)
2019-07-08 19:15:05 +02:00
cr:rectangle(sa.x - screen_x, sa.y - screen_y, sa.width, sa.height)
cr:fill()
2019-07-06 23:06:14 +02:00
cr:set_source(border_color)
2019-07-08 19:15:05 +02:00
cr:rectangle(sa.x - screen_x, sa.y - screen_y, sa.width, sa.height)
cr:set_line_width(10.0)
cr:stroke()
cr:reset_clip()
2019-07-04 23:32:05 +02:00
end
for i, a in ipairs(open_areas) do
local sa = shrink_area_with_gap(a, gap)
2019-07-08 19:15:05 +02:00
cr:rectangle(sa.x - screen_x, sa.y - screen_y, sa.width, sa.height)
cr:clip()
if i == #open_areas then
cr:set_source(api.gears.color(active_color))
else
cr:set_source(api.gears.color(open_color))
2019-07-04 23:32:05 +02:00
end
2019-07-08 19:15:05 +02:00
cr:rectangle(sa.x - screen_x, sa.y - screen_y, sa.width, sa.height)
cr:fill()
2019-07-06 23:06:14 +02:00
cr:set_source(border_color)
2019-07-08 19:15:05 +02:00
cr:rectangle(sa.x - screen_x, sa.y - screen_y, sa.width, sa.height)
cr:set_line_width(10.0)
if i ~= #open_areas then
cr:set_dash({5, 5}, 0)
cr:stroke()
cr:set_dash({}, 0)
else
cr:stroke()
2019-07-04 23:32:05 +02:00
end
cr:reset_clip()
end
2019-07-04 23:32:05 +02:00
cr:select_font_face(label_font_family, "normal", "normal")
cr:set_font_size(info_size)
cr:set_font_face(cr:get_font_face())
msg = current_info
ext = cr:text_extents(msg)
cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing)
cr:text_path(msg)
cr:set_source_rgba(1, 1, 1, 1)
cr:fill()
cr:move_to(width / 2 - ext.width / 2 - ext.x_bearing, height / 2 - ext.height / 2 - ext.y_bearing)
cr:text_path(msg)
cr:set_source_rgba(0, 0, 0, 1)
cr:set_line_width(2.0)
cr:stroke()
end
2019-07-04 23:32:05 +02:00
local function refresh()
print("closed areas:")
for i, a in ipairs(closed_areas) do
print(" " .. _area_tostring(a))
end
print("open areas:")
for i, a in ipairs(open_areas) do
print(" " .. _area_tostring(a))
end
infobox.bgimage = draw_info
end
2019-07-04 23:32:05 +02:00
print("interactive layout editing starts")
2019-07-04 23:32:05 +02:00
init(screen.workarea)
refresh()
kg = keygrabber.run(function (mod, key, event)
if event == "release" then
return
2019-07-04 23:32:05 +02:00
end
if key == "BackSpace" then
pop_history()
elseif key == "Escape" then
2019-07-04 23:32:05 +02:00
table.remove(data.cmds, #data.cmds)
to_exit = true
elseif key == "Up" or key == "Down" then
if current_cmd ~= data.cmds[cmd_index] then
data.cmds[#data.cmds] = current_cmd
2019-07-04 23:32:05 +02:00
end
if key == "Up" and cmd_index > 1 then
cmd_index = cmd_index - 1
elseif key == "Down" and cmd_index < #data.cmds then
cmd_index = cmd_index + 1
2019-07-05 01:21:08 +02:00
end
print("restore history #" .. tostring(cmd_index) .. ":" .. data.cmds[cmd_index])
init(screen.workarea)
for i = 1, #data.cmds[cmd_index] do
cmd = data.cmds[cmd_index]:sub(i, i)
push_history()
local ret = handle_command(cmd)
2019-07-09 21:04:20 +02:00
if ret == nil then
print("warning: ret is nil")
else
current_info = current_info .. ret
current_cmd = current_cmd .. ret
end
end
if #open_areas == 0 then
current_info = current_info .. " (enter to save)"
end
elseif #open_areas > 0 then
push_history()
local ret = handle_command(key)
if ret ~= nil then
current_info = current_info .. ret
current_cmd = current_cmd .. ret
else
discard_history()
end
if #open_areas == 0 then
current_info = current_info .. " (enter to save)"
end
else
if key == "Return" then
2019-07-09 21:04:20 +02:00
local layout = api.layout.get(screen)
table.remove(data.cmds, #data.cmds)
-- remove duplicated entries
local j = 1
for i = 1, #data.cmds do
if data.cmds[i] ~= current_cmd then
data.cmds[j] = data.cmds[i]
j = j + 1
2019-07-05 01:09:17 +02:00
end
end
for i = #data.cmds, j, -1 do
table.remove(data.cmds, i)
end
-- bring the current cmd to the front
data.cmds[#data.cmds + 1] = current_cmd
2019-07-08 22:58:09 +02:00
data.last_cmd[layout.name] = current_cmd
if data.history_file then
local file, err = io.open(data.history_file, "w")
if err then
print("cannot save history to " .. data.history_file)
else
for i = max(1, #data.cmds - data.history_save_max + 1), #data.cmds do
print("save cmd " .. data.cmds[i])
file:write(data.cmds[i] .. "\n")
end
2019-07-08 22:58:09 +02:00
for name, cmd in pairs(data.last_cmd) do
print("save last cmd " .. cmd .. " for " .. name)
file:write("+" .. name .. "\n" .. cmd .. "\n")
end
end
2019-07-09 21:04:20 +02:00
file:close()
end
current_info = "Saved!"
to_exit = true
to_apply = true
2019-07-05 01:09:17 +02:00
end
2019-07-04 23:32:05 +02:00
end
refresh()
2019-07-04 23:32:05 +02:00
if to_exit then
print("interactive layout editing ends")
if to_apply then
local layout = api.layout.get(screen)
if layout.set_regions then
local areas_with_gap = {}
for _, a in ipairs(closed_areas) do
areas_with_gap[#areas_with_gap + 1] = shrink_area_with_gap(a, gap)
2019-07-05 21:08:17 +02:00
end
table.sort(
areas_with_gap,
function (a1, a2)
local s1 = a1.width * a1.height
local s2 = a2.width * a2.height
if math.abs(s1 - s2) < 0.01 then
return (a1.x + a1.y) < (a2.x + a2.y)
else
return s1 > s2
end
end
)
layout.cmd = current_cmd
layout.set_regions(areas_with_gap)
api.layout.arrange(screen)
end
api.gears.timer{
timeout = 1,
autostart = true,
singleshot = true,
callback = cleanup
}
else
cleanup()
2019-07-04 23:32:05 +02:00
end
keygrabber.stop(kg)
return
end
end)
end
local function set_by_cmd(layout, screen, cmd)
init(screen.workarea)
push_history()
for i = 1, #cmd do
local key = handle_command(cmd:sub(i, i))
end
local areas_with_gap = {}
for _, a in ipairs(closed_areas) do
areas_with_gap[#areas_with_gap + 1] = shrink_area_with_gap(a, gap)
end
table.sort(
areas_with_gap,
function (a1, a2)
local s1 = a1.width * a1.height
local s2 = a2.width * a2.height
if math.abs(s1 - s2) < 0.01 then
return (a1.x + a1.y) < (a2.x + a2.y)
2019-07-04 23:32:05 +02:00
else
return s1 > s2
2019-07-04 23:32:05 +02:00
end
end
)
layout.cmd = cmd
2019-07-08 22:58:09 +02:00
data.last_cmd[layout.name] = cmd
layout.set_regions(areas_with_gap)
api.layout.arrange(screen)
end
2019-07-08 23:32:45 +02:00
local function refresh_layout(layout, screen)
if layout.cmd == nil then return end
set_by_cmd(layout, screen, layout.cmd)
end
local function try_restore_last(layout, screen)
2019-07-08 22:58:09 +02:00
if data.last_cmd[layout.name] == nil then return end
set_by_cmd(layout, screen, data.last_cmd[layout.name])
end
return {
start_interactive = start_interactive,
2019-07-08 23:32:45 +02:00
refresh_layout = refresh_layout,
try_restore_last = try_restore_last,
}
2019-07-04 23:32:05 +02:00
end
return
{
set_region = set_region,
2019-07-06 06:16:58 +02:00
fit_region = fit_region,
create = create,
2019-07-05 01:09:17 +02:00
restore_data = restore_data,
2019-07-04 23:32:05 +02:00
}