awesome/lib/layout-machi/switcher.lua

842 lines
27 KiB
Lua

local machi = {
layout = require((...):match("(.-)[^%.]+$") .. "layout"),
engine = require((...):match("(.-)[^%.]+$") .. "engine"),
}
local capi = {
client = client,
screen = screen,
}
local beautiful = require("beautiful")
local wibox = require("wibox")
local awful = require("awful")
local gears = require("gears")
local lgi = require("lgi")
local dpi = require("beautiful.xresources").apply_dpi
local gtimer = require("gears.timer")
local ERROR = 2
local WARNING = 1
local INFO = 0
local DEBUG = -1
local module = {
log_level = WARNING,
}
local function log(level, msg)
if level > module.log_level then
print(msg)
end
end
local function min(a, b)
if a < b then return a else return b end
end
local function max(a, b)
if a < b then return b else return a end
end
local function with_alpha(col, alpha)
local r, g, b
_, r, g, b, _ = col:get_rgba()
return lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
end
function module.start(c, exit_keys)
local tablist_font_desc = beautiful.get_merged_font(
beautiful.font, dpi(10))
local font_color = with_alpha(gears.color(beautiful.fg_normal), 1)
local font_color_hl = with_alpha(gears.color(beautiful.fg_focus), 1)
local label_size = dpi(30)
local border_color = with_alpha(
gears.color(beautiful.machi_switcher_border_color or beautiful.border_focus),
beautiful.machi_switcher_border_opacity or 0.25)
local border_color_hl = with_alpha(
gears.color(beautiful.machi_switcher_border_hl_color or beautiful.border_focus),
beautiful.machi_switcher_border_hl_opacity or 0.75)
local fill_color = with_alpha(
gears.color(beautiful.machi_switcher_fill_color or beautiful.bg_normal),
beautiful.machi_switcher_fill_opacity or 0.25)
local box_bg = with_alpha(
gears.color(beautiful.machi_switcher_box_bg or beautiful.bg_normal),
beautiful.machi_switcher_box_opacity or 0.85)
local fill_color_hl = with_alpha(
gears.color(beautiful.machi_switcher_fill_color_hl or beautiful.bg_focus),
beautiful.machi_switcher_fill_hl_opacity or 1)
-- for comparing floats
local threshold = 0.1
local traverse_radius = dpi(5)
local screen = c and c.screen or awful.screen.focused()
local tag = screen.selected_tag
local layout = tag.layout
local gap = tag.gap
local start_x = screen.workarea.x
local start_y = screen.workarea.y
if layout.machi_get_instance_data == nil then return end
local cd, td, areas, _new_placement_cb = layout.machi_get_instance_data(screen, screen.selected_tag)
if areas == nil or #areas == 0 then
return
end
local infobox, infotabbox
local tablist = nil
local traverse_x, traverse_y
if c then
traverse_x = c.x + traverse_radius
traverse_y = c.y + traverse_radius
else
traverse_x = screen.workarea.x + screen.workarea.width / 2
traverse_y = screen.workarea.y + screen.workarea.height / 2
end
local selected_area_ = nil
local function set_selected_area(area)
selected_area_ = area
if area then
traverse_x = max(areas[area].x + traverse_radius, min(areas[area].x + areas[area].width - traverse_radius, traverse_x))
traverse_y = max(areas[area].y + traverse_radius, min(areas[area].y + areas[area].height - traverse_radius, traverse_y))
end
end
local function selected_area()
if selected_area_ == nil then
local min_dis = nil
for i, a in ipairs(areas) do
if a.habitable then
local dis =
math.abs(a.x + traverse_radius - traverse_x) + math.abs(a.x + a.width - traverse_radius - traverse_x) - a.width +
math.abs(a.y + traverse_radius - traverse_y) + math.abs(a.y + a.height - traverse_radius - traverse_y) - a.height +
traverse_radius * 4
if min_dis == nil or min_dis > dis then
min_dis = dis
selected_area_ = i
end
end
end
set_selected_area(selected_area_)
end
return selected_area_
end
local parent_stack = {}
local function get_tablist(area)
local list = {}
for _, tc in ipairs(screen.tiled_clients) do
if not (tc.floating or tc.immobilized)
then
if areas[area].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= areas[area].x + areas[area].width and
areas[area].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= areas[area].y + areas[area].height
then
list[#list + 1] = tc
end
end
end
return list
end
local function maintain_tablist()
tablist = get_tablist(selected_area())
if #tablist ~= 0 then
c = tablist[1]
end
end
local function draw_tab_info(context, cr, width, height)
maintain_tablist()
local wi = { x = 0, y = 0, w = 1, h = 1 }
local ext
local active_area = selected_area()
if #tablist > 0 then
local a = areas[active_area]
local pl = lgi.Pango.Layout.create(cr)
pl:set_font_description(tablist_font_desc)
local vpadding = dpi(10)
local hpadding = dpi(15)
local vborder = dpi(5)
local hborder = dpi(5)
local list_height = 2 * vborder
local list_width = 2 * hpadding + 2 * hborder
local exts = {}
local tl = {}
local hw = math.floor((#tablist-1)/2)
for i = hw+1,2,-1 do
table.insert(tl, tablist[i])
end
table.insert(tl, tablist[1])
local active = #tl
for i = #tablist,hw+2,-1 do
table.insert(tl, tablist[i])
end
for index, tc in ipairs(tl) do
local label = tc.name or "<unnamed>"
pl:set_text(label)
local w, h
w, h = pl:get_size()
w = w / lgi.Pango.SCALE
h = h / lgi.Pango.SCALE
local ext = { width = w, height = h }
exts[#exts + 1] = ext
list_height = list_height + ext.height + vpadding
list_width = max(list_width, w + 2 * hpadding + 2 * hborder)
end
local y_offset = vborder
wi.x = a.x + (a.width - list_width) / 2
wi.y = a.y + (a.height - list_height) / 2
wi.w = list_width
wi.h = list_height
cr:rectangle(0, 0, list_width, list_height)
cr:set_source(border_color_hl)
cr:fill()
for index, tc in ipairs(tl) do
local label = tc.name or "<unnamed>"
local ext = exts[index]
if index == active then
cr:rectangle(hborder, y_offset, list_width - 2 * hborder, ext.height + vpadding)
cr:set_source(fill_color_hl)
cr:fill()
pl:set_text(label)
cr:move_to(hborder + hpadding, vpadding / 2 + y_offset)
cr:set_source(font_color_hl)
cr:show_layout(pl)
else
pl:set_text(label)
cr:move_to(hborder + hpadding, vpadding / 2 + y_offset)
cr:set_source(font_color)
cr:show_layout(pl)
end
y_offset = y_offset + ext.height + vpadding
end
end
if infotabbox.x ~= wi.x or infotabbox.y ~= wi.y or
infotabbox.width ~= wi.w or infotabbox.height ~= wi.h
then
infotabbox.x = wi.x
infotabbox.y = wi.y
infotabbox.width = wi.w
infotabbox.height = wi.h
end
end
local function draw_info(context, cr, width, height)
maintain_tablist()
cr:set_source_rgba(0, 0, 0, 0)
cr:rectangle(0, 0, width, height)
cr:fill()
local padx = 5
local pady = 5
local msg
local active_area = selected_area()
for i, a in ipairs(areas) do
if a.habitable or i == active_area then
cr:rectangle(a.x - padx - start_x, a.y - pady - start_y, a.width + 2*padx, a.height + 2*pady)
cr:clip()
cr:set_source(fill_color)
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
cr:fill()
cr:set_source(i == active_area and border_color_hl or border_color)
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
cr:set_line_width(10.0)
cr:stroke()
cr:reset_clip()
end
end
if infobox.width ~= screen.workarea.width or infobox.height ~= screen.workarea.height then
gtimer.delayed_call(function ()
infobox.width = screen.workarea.width
infobox.height = screen.workarea.height
infobox.visible = true
end)
end
-- show the traverse point
-- cr:rectangle(traverse_x - start_x - traverse_radius, traverse_y - start_y - traverse_radius, traverse_radius * 2, traverse_radius * 2)
-- cr:set_source_rgba(1, 1, 1, 1)
-- cr:fill()
end
local draw = function()
if infobox ~= nil then
infobox.bgimage = draw_info
end
if infotabbox ~= nil then
infotabbox.bgimage = draw_tab_info
end
end
local largest_area = function(areas)
local largest = nil
local size = 0
for i, a in ipairs(areas) do
local s = a.width * a.height
if a.habitable and (largest == nil or s > size) then
largest = i
size = s
end
end
return largest
end
local snap_area = function(x, y, width, height)
local bestD, d
local best
local dist = function(x0, y0, x1, y1)
local a, b = x0-x1, y0-y1
return math.sqrt(a*a+b*b)
end
for i, a in ipairs(areas) do
d = dist(a.x, a.y, x, y) +
dist(a.x+a.width, a.y+a.height, x+width, y+height)
if bestD == nil or d < bestD then
bestD = d
best = i
end
end
return best
end
-- area is the index in areas, should be made consistent after el big refactor
-- draft is reset
local move_client
move_client = function(c, area)
if area == nil or areas[area] == nil or c == nil or not c.valid or c.floating then
return
end
machi.layout.set_geometry(c, areas[area], areas[area], 0, c.border_width)
if cd[c] == nil then
return
end
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = area
end
local snap_client = function()
if c == nil then
return
end
a = snap_area(c.x, c.y, c.width, c.height)
if a == nil then
return false
end
move_client(c, a)
return true
end
local master_add = function()
if c == nil or c.floating then
return
end
local dst = largest_area(areas)
if dst == nil then
return
end
move_client(c, dst)
awful.layout.arrange(screen)
end
local master_swap = function(all)
if c == nil or c.floating then
return
end
local src = selected_area()
local dst = largest_area(areas)
if dst == nil or dst == src then
return
end
local srclist = get_tablist(src)
local dstlist = get_tablist(dst)
for i, c in ipairs(dstlist) do
if all or i == 1 then
move_client(c, src)
end
end
for i, c in ipairs(srclist) do
if all or i == 1 then
move_client(c, dst)
end
end
awful.layout.arrange(screen)
end
local traverse_clients = function(dir)
dir = string.lower(dir)
local current_area = selected_area()
local current = areas[current_area]
if current == nil then
return false
end
local candidates = {}
for i, a in ipairs(areas) do
local data = {i = i, a = a}
if not a.habitable or i == current_area then
elseif dir == "up" and
a.y + a.height < current.y and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
elseif dir == "down" and
a.y > current.y + current.height and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
elseif dir == "left" and
a.x + a.width < current.x and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
elseif dir == "right" and
a.x > current.x + current.width and
#get_tablist(i) ~= 0 then
table.insert(candidates, data)
end
end
local best_area
local score
for i, d in ipairs(candidates) do
local a = d.a
local dist, overlap
if dir == "up" or dir == "down" then
overlap = min(current.x+current.width, a.x+a.width) - max(current.x, a.x)
elseif dir == "left" or dir == "right" then
overlap = min(current.y+current.height, a.y+a.height) - max(current.y, a.y)
end
if dir == "up" then
dist = current.y - a.y - a.height
elseif dir == "down" then
dist = a.y - current. y - current.height
elseif dir == "left" then
dist = current.x - a.x - a.width
elseif dir == "right" then
dist = a.x - current.x - current.width
end
-- TODO this is probably not optimal
local s = overlap - dist
if score == nil or s > score then
best_area = d.i
score = s
end
end
if best_area == nil then
if #candidates == 0 then
return false
end
best_area = candidates[1].i
end
set_selected_area(best_area)
local tablist = get_tablist(best_area)
if #tablist == 0 or tablist[1] == c then
return false
end
c = tablist[1]
capi.client.focus = c
return true
end
local traverse_areas = function(dir, move, draft)
dir = string.lower(dir)
local current_area = selected_area()
if c and move then
if current_area == nil or
areas[current_area].x ~= c.x or
areas[current_area].y ~= c.y
then
traverse_x = c.x + traverse_radius
traverse_y = c.y + traverse_radius
set_selected_area(nil)
end
elseif c and draft then
local ex = c.x + c.width + c.border_width * 2
local ey = c.y + c.height + c.border_width * 2
if current_area == nil or
areas[current_area].x + areas[current_area].width ~= ex or
areas[current_area].y + areas[current_area].height ~= ey
then
traverse_x = ex - traverse_radius
traverse_y = ey - traverse_radius
set_selected_area(nil)
end
end
local choice = nil
local choice_value
current_area = selected_area()
for i, a in ipairs(areas) do
if not a.habitable then goto continue end
local v
if dir == "up" then
if a.x < traverse_x + threshold
and traverse_x < a.x + a.width + threshold then
v = traverse_y - a.y - a.height
else
v = -1
end
elseif dir == "down" then
if a.x < traverse_x + threshold
and traverse_x < a.x + a.width + threshold then
v = a.y - traverse_y
else
v = -1
end
elseif dir == "left" then
if a.y < traverse_y + threshold
and traverse_y < a.y + a.height + threshold then
v = traverse_x - a.x - a.width
else
v = -1
end
elseif dir == "right" then
if a.y < traverse_y + threshold
and traverse_y < a.y + a.height + threshold then
v = a.x - traverse_x
else
v = -1
end
end
if (v > threshold) and (choice_value == nil or choice_value > v) then
choice = i
choice_value = v
end
::continue::
end
if choice == nil then
choice = current_area
if dir == "up" then
traverse_y = screen.workarea.y
elseif dir == "down" then
traverse_y = screen.workarea.y + screen.workarea.height
elseif dir == "left" then
traverse_x = screen.workarea.x
else
traverse_x = screen.workarea.x + screen.workarea.width
end
end
if choice ~= nil then
tablist = nil
set_selected_area(choice)
if c and draft and cd[c].draft ~= false then
local lu = cd[c].lu or cd[c].area
local rd = cd[c].rd or cd[c].area
if draft and move then
lu = choice
if areas[rd].x + areas[rd].width <= areas[lu].x or
areas[rd].y + areas[rd].height <= areas[lu].y
then
rd = nil
end
else
rd = choice
if areas[rd].x + areas[rd].width <= areas[lu].x or
areas[rd].y + areas[rd].height <= areas[lu].y
then
lu = nil
end
end
if lu ~= nil and rd ~= nil then
machi.layout.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
elseif lu ~= nil then
machi.layout.set_geometry(c, areas[lu], nil, 0, c.border_width)
elseif rd ~= nil then
c.x = min(c.x, areas[rd].x)
c.y = min(c.y, areas[rd].y)
machi.layout.set_geometry(c, nil, areas[rd], 0, c.border_width)
end
if lu == rd and cd[c].draft ~= true then
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = lu
else
cd[c].lu = lu
cd[c].rd = rd
cd[c].area = nil
end
c:emit_signal("request::activate", "mouse.move", {raise=false})
c:raise()
awful.layout.arrange(screen)
elseif c and move then
-- move the window
local in_draft = cd[c].draft
if cd[c].draft ~= nil then
in_draft = cd[c].draft
elseif cd[c].lu then
in_draft = true
elseif cd[c].area then
in_draft = false
else
log(ERROR, "Assuming in_draft for unhandled client "..tostring(c))
in_draft = true
end
if in_draft then
c.x = areas[choice].x
c.y = areas[choice].y
else
machi.layout.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
cd[c].lu = nil
cd[c].rd = nil
cd[c].area = choice
end
c:emit_signal("request::activate", "mouse.move", {raise=false})
c:raise()
awful.layout.arrange(screen)
tablist = nil
else
maintain_tablist()
-- move the focus
if #tablist > 0 and tablist[1] ~= c then
c = tablist[1]
capi.client.focus = c
end
end
draw()
end
end
local tab = function()
maintain_tablist()
if #tablist > 1 then
c = tablist[#tablist]
c:emit_signal("request::activate", "mouse.move", {raise=false})
c:raise()
draw()
end
end
local function handle_key(mod, key, event)
local key_translate_tab = {
["k"] = "Up",
["h"] = "Left",
["j"] = "Down",
["l"] = "Right",
["K"] = "Up",
["H"] = "Left",
["J"] = "Down",
["L"] = "Right",
}
if event == "release" then
if exit_keys and exit_keys[key] then
exit()
end
return
end
if key_translate_tab[key] ~= nil then
key = key_translate_tab[key]
end
maintain_tablist()
assert(tablist ~= nil)
local super = false
local shift = false
local ctrl = false
for i, m in ipairs(mod) do
if m == "Mod4" then super = true
elseif m == "Shift" then shift = true
elseif m == "Control" then ctrl = true
end
end
if key == "Tab" then
tab()
elseif key == "Up" or key == "Down" or key == "Left" or key == "Right" then
traverse_areas(key, shift or super, ctrl)
elseif (key == "q" or key == "Prior") then
local current_area = selected_area()
if areas[current_area].parent_id == nil then
return
end
set_selected_area(areas[current_area].parent_id)
if #parent_stack == 0 or
parent_stack[#parent_stack] ~= current_area then
parent_stack = {current_area}
end
parent_stack[#parent_stack + 1] = areas[current_area].parent_id
current_area = parent_stack[#parent_stack]
if c and ctrl and cd[c].draft ~= false then
if cd[c].area then
cd[c].lu, cd[c].rd, cd[c].area = cd[c].area, cd[c].area, nil
end
machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
awful.layout.arrange(screen)
end
draw()
elseif (key =="e" or key == "Next") then
local current_area = selected_area()
if #parent_stack <= 1 or parent_stack[#parent_stack] ~= current_area then
return
end
set_selected_area(parent_stack[#parent_stack - 1])
table.remove(parent_stack, #parent_stack)
current_area = parent_stack[#parent_stack]
if c and ctrl then
if areas[current_area].habitable and cd[c].draft ~= true then
cd[c].lu, cd[c].rd, cd[c].area = nil, nil, current_area
end
machi.layout.set_geometry(c, areas[current_area], areas[current_area], 0, c.border_width)
awful.layout.arrange(screen)
end
draw()
elseif key == "/" then
local current_area = selected_area()
local original_cmd = machi.engine.areas_to_command(areas, true, current_area)
areas[current_area].hole = true
local prefix, suffix = machi.engine.areas_to_command(
areas, false):match("(.*)|(.*)")
areas[current_area].hole = nil
workarea = {
x = areas[current_area].x - gap * 2,
y = areas[current_area].y - gap * 2,
width = areas[current_area].width + gap * 4,
height = areas[current_area].height + gap * 4,
}
gtimer.delayed_call(
function ()
layout.machi_editor.start_interactive(
screen,
{
workarea = workarea,
original_cmd = original_cmd,
cmd_prefix = prefix,
cmd_suffix = suffix,
}
)
end
)
exit()
elseif (key == "f" or key == ".") and c then
if cd[c].draft == nil then
cd[c].draft = true
elseif cd[c].draft == true then
cd[c].draft = false
else
cd[c].draft = nil
end
awful.layout.arrange(screen)
elseif key == "Escape" or key == "Return" then
exit()
else
log(DEBUG, "Unhandled key " .. key)
end
end
local ui = function()
infobox = wibox({
screen = screen,
x = screen.workarea.x,
y = screen.workarea.y,
width = 1, -- screen.workarea.width,
height = 1, --screen.workarea.height,
bg = "#ffffff00",
opacity = 1,
ontop = false,
type = "normal",
})
infotabbox = wibox({
screen = screen,
x = screen.workarea.x,
y = screen.workarea.y,
width = 1,
height = 1,
bg = "#ffffff00",
ontop = true,
type = "normal",
visible = true,
})
draw()
awful.client.focus.history.disable_tracking()
local kg
local function exit()
awful.client.focus.history.enable_tracking()
if capi.client.focus then
capi.client.emit_signal("focus", capi.client.focus)
end
infobox.visible = false
infotabbox.visible = false
awful.keygrabber.stop(kg)
end
kg = awful.keygrabber.run(
function (...)
ok, _ = pcall(handle_key, ...)
if not ok then exit() end
end
)
end
return {
ui = ui,
tab = tab,
snap_client = snap_client,
master_add = master_add,
--- swap current window with 'master' or moves it to the master position if it's unoccupied (i.e.: largest area/client)
-- @param all swap/move all clients in both areas, not just the top ones.
master_swap = master_swap,
--- focus client by direction
-- @param dir direction (up|right|down|left)
-- @returns true if a client was found
traverse_clients = traverse_clients,
traverse_areas = traverse_areas,
move = function(dir) traverse_areas(dir, true, false) end,
-- like traverse_clients, returns true if a client was found
focus = function(dir) return traverse_clients(dir) end,
}
end
return module