Refactor the engine out of editor. Support adjustment with various tweaks. Need to reduce the diff...

This commit is contained in:
Xinhao Yuan 2021-02-23 00:32:58 -05:00
parent a04e2d6e35
commit 40a20f08d2
6 changed files with 1745 additions and 1151 deletions

View File

@ -8,6 +8,23 @@ Draft mode: https://imgur.com/a/BOvMeQL
__Most of the development effort now happens in the ng branch, which introduces a few breaking changes but a ton of new features/enhancements.__
## Machi-ng
Machi-ng is a refactoring effort of machi with new features and enhancements.
### Breaking changes
1. Added a max split (before merging) of 1,000 for all commands and a global cap of 10,000 areas.
2. `t` command now applies to the current area and its further splits, instead of globally.
3. `s` command now shifts inside the last group of pending areas that have the same parent, instead of all pending areas.
### New features & enhancements
1. Minimum area size.
2. More tolerating "safer" error handling.
3. Dynamic size adjustment with propagation.
4. Editing non-global areas.
## Why?
TL;DR --- To bring back the control of the window layout.
@ -46,6 +63,7 @@ Use `local layout = machi.layout.create(args)` to instantiate the layout with an
- `name`: the constant name of the layout.
- `name_func`: a `function(t)` closure that returns a string for tag `t`. `name_func` overrides `name`.
- `icon_name`: the "system" name used by Awesome to find the icon. The default value is `machi`.
- `persistent`: whether to keep a history of the command for the layout. The default is `true`.
- `default_cmd`: the command to use if there is no persistent history for this layout.
- `editor`: the editor used for the layout. The default is `machi.default_editor` (or `machi.editor.default_editor`).
@ -59,19 +77,19 @@ The function is compatible with the previous `machi.layout.create(name, editor,
### Starting editor in lua
Call `local editor = machi.editor.create()` to create an editor.
To edit the layout `l` on screen `s`, call `editor.start_interactive(s, l)`.
Calling it with no arguments would be the same as `editor.start_interactive(awful.screen.focused(), awful.layout.get(awful.screen.focused()))`.
To edit the current machi layout on screen `s`, call `editor.start_interactive(s)`.
Calling it with no arguments would be the same as `editor.start_interactive(awful.screen.focused())`.
### Basic usage
The editing command starts with the open region of the entire workarea, perform "operations" to split the current region into multiple sub-regions, then recursively edits each of them (by default, the maximum split depth is 2).
The editing command starts with the open area of the entire workarea, perform "operations" to split the current area into multiple sub-areas, then recursively edits each of them (by default, the maximum split depth is 2).
The layout is defined by a sequence of operations as a layout command.
The layout editor allows users to interactively input their commands and shows the resulting layouts on screen, with the following auxiliary functions:
1. `Up`/`Down`: restore to the history command
2. `Backspace`: undo the last command.
2. `Backspace`: undo the last command. If the command is already empty, restores to the current (maybe transcoded) command of the layout.
3. `Escape`: exit the editor without saving the layout.
4. `Enter`: when all regions are defined, hit enter will save the layout.
4. `Enter`: when all areas are defined, hit enter will save the layout. If shift is hold, only applies the command without saving it to the history.
### Layout command
@ -80,15 +98,24 @@ There are three kinds of operations:
1. Operations taking argument string and parsed as multiple numbers.
`h` horizontally split, `v` vertically split, `w` grid split, `d` draft split
- `h`: horizontally split. Splits to two areas evenly without args.
- `v`: vertically split. Splits to two areas evenly without args.
- `w`: grid split. No splits without args.
- `d`: draft split. No splits without args.
2. Operations taking argument string as a single number.
2. Operations taking argument string as a single number or string.
`s` shift active region, `t` set the maximum split depth, `x` set the nested layout of the current region.
- `s`: shift open areas within the same parent. Shifts one area without args.
- `c`: finish the open areas within the same parent. Finishes all areas with the same parent without args.
- `t`: set the number of further split of the curret area. Sets to the default (2) splits without args.
- `x`: set the nested layout of the current area. Behaves like `-` without args.
3. Operation not taking argument.
`.` finish all regions, `-` finish the current region, `/` remove the current region, `;` no-op
- `.`: finish all areas.
- `-`: finish the current area
- `/`: remove the current area
- `;`: no-op
Argument strings are composed of numbers and `,`. If the string contains `,`, it will be used to split argument into multiple numbers.
Otherwise, each digit in the string will be treated as a separated number in type 1 ops.
@ -124,9 +151,9 @@ For examples:
Details:
- `131h`: horizontally split the initial region (entire desktop) to the ratio of 1:3:1
- `131h`: horizontally split the initial area (entire desktop) to the ratio of 1:3:1
- For the first `1` part:
- `2v`: vertically split the region to the ratio of 2:1
- `2v`: vertically split the area to the ratio of 2:1
- `-`: skip the editing of the middle `3` part
- For the right `1` part:
- `12v`: split the right part vertically to the ratio of 1:2
@ -229,15 +256,17 @@ Final merge, size 3x1, `w443113132231`:
2 4-4-4
```
`d` command works similarly after the inital grid is defined, such as `d1221012210221212121222`.
### Draft mode
__This mode is somewhat usable, yet it may change in the future.__
Unlike the original machi layout, where a window fits in a single region, draft mode allows window to span across multiple regions.
Each tiled window is associated with a upper-left region (ULR) and a bottom-right region (BRR).
The geometry of the window is from the upper-left corner of the ULR to the bottom-right corner of the BRR.
Unlike the original machi layout, where a window fits in a single area, draft mode allows window to span across multiple areas.
Each tiled window is associated with a upper-left area (UL) and a bottom-right area (BR).
The geometry of the window is from the upper-left corner of the UL to the bottom-right corner of the BR.
This is suppose to work with regions produced with `d` or `w` operation.
This is suppose to work with areas produced with `d` or `w` operation.
To enable draft mode in a layout, configure the layout with a command with a leading `d`, for example, `d12210121`, or `dw66`.
### Nested layouts
@ -253,7 +282,7 @@ Known caveats include:
__This feature is not available in draft mode.__
To set up nested layouts, you first need to check/modify `machi.editor.nested_layouts` array, which maps an argument string (`[0-9,]+`) to a layout object.
In machi command, use the argument string with command `x` will set up the nested layout of the region to the mapped one.
In machi command, use the argument string with command `x` will set up the nested layout of the area to the mapped one.
For example, since by default `machi.editor.nested_layouts["0"]` is `awful.layout.suit.tile` and `machi.editor.nested_layouts["1"]` is `awful.layout.suit.spiral`,
the command `11h0x1x` will split the screen horizontally and apply the layouts accordingly - see the figure below.
@ -269,18 +298,19 @@ To change that, please refer to `editor.lua`. (XXX more documents)
Calling `machi.switcher.start()` will create a switcher supporting the following keys:
- Arrow keys: move focus into other regions by the direction.
- `Shift` + arrow keys: move the focused window to other regions by the direction. In draft mode, move the window while preserving its size.
- `Control`[ + `Shift`] + arrow keys: move the bottom-right (or top-left window if `Shift` is pressed) region of the focused window by direction. Only works in draft mode.
- `Tab`: switch beteen windows covering the current regions.
- Arrow keys: move focus into other areas by the direction.
- `Shift` + arrow keys: move the focused window to other areas by the direction. In draft mode, move the window while preserving its size.
- `Control`[ + `Shift`] + arrow keys: move the bottom-right (or top-left window if `Shift` is pressed) area of the focused window by direction. Only works in draft mode.
- `Tab`: switch beteen windows covering the current areas.
- `u` or `PageUp` (`Prior`): In non-draft mode, you can select the parent of the current area.
- `/`: In non-draft mode, this opens the editor to edit the selected area using the same command interpretation.
Note the final command may be transcoded to be embeddable, but the areas shall be the same.
So far, the key binding is not configurable. One has to modify the source code to change it.
## Caveats
1. layout-machi handles `beautiful.useless_gap` slightly differently.
2. A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients.
A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients.
## License

1035
editor.lua

File diff suppressed because it is too large Load Diff

937
engine.lua Normal file
View File

@ -0,0 +1,937 @@
-- area {
-- x, y, width, height
-- parent_id
-- parent_cid
-- parent_x_shares
-- parent_y_shares
-- inhabitable
-- hole (unique)
-- draft_mode (root only)
-- }
--
-- split {
-- method
-- x_shares
-- y_shares
-- children
-- }
--
-- share {weight, adjustment, dynamic, minimum}
local in_module = ...
-- Split a length by `measures`, such that each split respect the
-- weight [1], adjustment (user [2] + engine [3]) without breaking the minimum size [4].
--
-- The split algorithm has a worst case of O(n^2) where n = #shares,
-- which should be fine for practical usage of screen partitions.
-- Using geometric algorithm this can be optimized to O(n log n), but
-- I don't think it is worth.
-- Returns two values:
-- 1. the (accumulative) result if it is possible to give every share its minimum size, otherwise nil.
-- 2. any spare space to adjust without capping any share.
local function fair_split(length, shares)
local ret = {}
local normalized_adj = nil
local sum_weight
local sum_adj
local remaining = #shares
local spare = nil
repeat
local need_recompute = false
sum_weight = 0
sum_adj = 0
for i = 1, #shares do
if ret[i] == nil then
sum_weight = sum_weight + shares[i][1]
if normalized_adj then
sum_adj = sum_adj + normalized_adj[i]
end
end
end
if normalized_adj == nil then
normalized_adj = {}
for i = 1, #shares do
if sum_weight > shares[i][1] then
normalized_adj[i] = ((shares[i][2] or 0) + (shares[i][3] or 0)) * sum_weight / (sum_weight - shares[i][1])
else
normalized_adj[i] = 0
end
sum_adj = sum_adj + normalized_adj[i]
end
for i = 1, #shares do
local required = (shares[i][4] - normalized_adj[i]) * sum_weight / shares[i][1] + sum_adj
if spare == nil or spare > length - required then
spare = length - required
end
end
end
local capped_length = 0
for i = 1, #shares do
if ret[i] == nil then
local split = (length - sum_adj) * shares[i][1] / sum_weight + normalized_adj[i]
if split < shares[i][4] then
ret[i] = shares[i][4]
capped_length = capped_length + shares[i][4]
need_recompute = true
end
end
end
length = length - capped_length
until not need_recompute
if #shares == 1 or spare < 0 then
spare = 0
end
if remaining == 0 then
return nil, spare
end
local acc_weight = 0
local acc_adj = 0
local acc_ret = 0
for i = 1, #shares do
if ret[i] == nil then
acc_weight = acc_weight + shares[i][1]
acc_adj = acc_adj + normalized_adj[i]
ret[i] = remaining == 1 and length - acc_ret or math.floor((length - sum_adj) / sum_weight * acc_weight + acc_adj - acc_ret + 0.5)
acc_ret = acc_ret + ret[i]
remaining = remaining - 1
end
end
ret[0] = 0
for i = 1, #shares do
ret[i] = ret[i - 1] + ret[i]
end
return ret, spare
end
-- Static data
-- Command character info
-- 3 for taking the arg string and an open area
-- 2 for taking an open area
-- 1 for taking nothing
-- 0 for args
local ch_info = {
["h"] = 3, ["H"] = 3,
["v"] = 3, ["V"] = 3,
["w"] = 3, ["W"] = 3,
["d"] = 3, ["D"] = 3,
["s"] = 3,
["t"] = 3,
["c"] = 3,
["x"] = 3,
["-"] = 2,
["/"] = 2,
["."] = 1,
[";"] = 1,
["0"] = 0, ["1"] = 0, ["2"] = 0, ["3"] = 0, ["4"] = 0,
["5"] = 0, ["6"] = 0, ["7"] = 0, ["8"] = 0, ["9"] = 0,
["_"] = 0, [","] = 0,
}
local function parse_arg_str(arg_str, default)
local ret = {}
local current = {}
if #arg_str == 0 then return ret end
local index = 1
local split_mode = arg_str:find("[,_]") ~= nil
local p = index
while index <= #arg_str do
local ch = arg_str:sub(index, index)
if split_mode then
if ch == "_" then
local r = tonumber(arg_str:sub(p, index - 1))
if r == nil then
current[#current + 1] = default
else
current[#current + 1] = r
end
p = index + 1
elseif ch == "," then
local r = tonumber(arg_str:sub(p, index - 1))
if r == nil then
current[#current + 1] = default
else
current[#current + 1] = r
end
ret[#ret + 1] = current
current = {}
p = index + 1
end
else
local r = tonumber(ch)
if r == nil then
ret[#ret + 1] = {default}
else
ret[#ret + 1] = {r}
end
end
index = index + 1
end
if split_mode then
local r = tonumber(arg_str:sub(p, index - 1))
if r == nil then
current[#current + 1] = default
else
current[#current + 1] = r
end
ret[#ret + 1] = current
end
return ret
end
if not in_module then
print("Testing parse_arg_str")
local x = parse_arg_str("1234", 0)
assert(#x == 4)
assert(#x[1] == 1 and x[1][1] == 1)
assert(#x[2] == 1 and x[2][1] == 2)
assert(#x[3] == 1 and x[3][1] == 3)
assert(#x[4] == 1 and x[4][1] == 4)
local x = parse_arg_str("12_34_,", -1)
assert(#x == 2)
assert(#x[1] == 3 and x[1][1] == 12 and x[1][2] == 34 and x[1][3] == -1)
assert(#x[2] == 1 and x[2][1] == -1)
local x = parse_arg_str("12_34,56_,78_90_", -1)
assert(#x == 3)
assert(#x[1] == 2 and x[1][1] == 12 and x[1][2] == 34)
assert(#x[2] == 2 and x[2][1] == 56 and x[2][2] == -1)
assert(#x[3] == 3 and x[3][1] == 78 and x[3][2] == 90 and x[3][3] == -1)
print("Passed.")
end
local max_split = 1000
local max_areas = 10000
local default_expansion = 2
-- Execute a (partial) command, returns:
-- 1. Closed areas: areas that will not be further partitioned by further input.
-- 2. Open areas: areas that can be further partitioned.
-- 3. Pending: if the command can take more argument into the last command.
local function areas_from_command(command, workarea, minimum)
local pending_op = nil
local arg_str = ""
local closed_areas = {}
local open_areas
local root = {
expansion = default_expansion,
x = workarea.x,
y = workarea.y,
width = workarea.width,
height = workarea.height,
bl = true,
br = true,
bu = true,
bd = true,
}
local function close_area()
local a = open_areas[#open_areas]
table.remove(open_areas, #open_areas)
local i = #closed_areas + 1
closed_areas[i] = a
a.id = i
return a, i
end
local function push_open_areas(areas)
for i = #areas, 1, -1 do
open_areas[#open_areas + 1] = areas[i]
end
end
local function handle_op(method)
local l = method:lower()
local alt = method ~= l
method = l
if method == "h" or method == "v" then
local args = parse_arg_str(arg_str, 0)
if #args == 0 then
args = {{1}, {1}}
elseif #args == 1 then
args[2] = {1}
end
local total = 0
local shares = { }
for i = 1, #args do
local arg
if not alt then
arg = args[i]
else
arg = args[#args - i + 1]
end
if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end
shares[i] = arg
end
if #shares > max_split then
return nil
end
local a, area_index = close_area()
a.inhabitable = true
a.split = {
method = method,
x_shares = method == "h" and shares or {{1}},
y_shares = method == "v" and shares or {{1}},
children = {}
}
local children = a.split.children
if method == "h" then
for i = 1, #a.split.x_shares do
local child = {
parent_id = area_index,
parent_cid = #children + 1,
parent_x_shares = #children + 1,
parent_y_shares = 1,
expansion = a.expansion - 1,
bl = i == 1 and a.bl or false,
br = i == #a.split.x_shares and a.br or false,
bu = a.bu,
bd = a.bd,
}
children[#children + 1] = child
end
else
for i = 1, #a.split.y_shares do
local child = {
parent_id = area_index,
parent_cid = #children + 1,
parent_x_shares = 1,
parent_y_shares = #children + 1,
expansion = a.expansion - 1,
bl = a.bl,
br = a.br,
bu = i == 1 and a.bu or false,
bd = i == #a.split.y_shares and a.bd or false,
}
children[#children + 1] = child
end
end
push_open_areas(children)
elseif method == "w" or method == "d" then
local args = parse_arg_str(arg_str, 0)
local x_shares = {}
local y_shares = {}
local m_start = #args + 1
if method == "w" then
if #args == 0 then
args = {{1}, {1}}
elseif #args == 1 then
args[2] = {1}
end
local x_shares_count, y_shares_count
if alt then
x_shares_count = args[2][1]
y_shares_count = args[1][1]
else
x_shares_count = args[1][1]
y_shares_count = args[2][1]
end
if x_shares_count < 1 then x_shares_count = 1 end
if y_shares_count < 1 then y_shares_count = 1 end
if x_shares_count * y_shares_count > max_split then
return nil
end
for i = 1, x_shares_count do x_shares[i] = {1} end
for i = 1, y_shares_count do y_shares[i] = {1} end
m_start = 3
else
local current = x_shares
for i = 1, #args do
if not alt then
arg = args[i]
else
arg = args[#args - i + 1]
end
if arg[1] == 0 then
if current == x_shares then current = y_shares else
m_start = i + 1
break
end
else
if arg[2] == 0 and arg[3] then arg[2], arg[3] = -arg[3], nil end
current[#current + 1] = arg
end
end
if #x_shares == 0 then
x_shares = {{1}}
end
if #y_shares == 0 then
y_shares = {{1}}
end
if #x_shares * #y_shares > max_split then
return nil
end
end
local a, area_index = close_area()
a.inhabitable = true
a.split = {
method = method,
x_shares = x_shares,
y_shares = y_shares,
children = {},
}
local children = {}
for y_index = 1, #a.split.y_shares do
for x_index = 1, #a.split.x_shares do
local r = {
parent_id = area_index,
-- parent_cid will be filled later.
parent_x_shares = x_index,
parent_y_shares = y_index,
expansion = a.expansion - 1
}
if x_index == 1 then r.bl = a.bl else r.bl = false end
if x_index == #a.split.x_shares then r.br = a.br else r.br = false end
if y_index == 1 then r.bu = a.bu else r.bu = false end
if y_index == #a.split.y_shares then r.bd = a.bd else r.bd = false end
children[#children + 1] = r
end
end
local merged_children = {}
local start_index = 1
for i = m_start, #args - 1, 2 do
-- find the first index that is not merged
while start_index <= #children and children[start_index] == false do
start_index = start_index + 1
end
if start_index > #children or children[start_index] == false then
break
end
local x = (start_index - 1) % #x_shares
local y = math.floor((start_index - 1) / #x_shares)
local w = args[i][1]
local h = args[i + 1][1]
if w < 1 then w = 1 end
if h == nil or h < 1 then h = 1 end
if alt then
local tmp = w
w = h
h = tmp
end
if x + w > #x_shares then w = #x_shares - x end
if y + h > #y_shares then h = #y_shares - y end
local end_index = start_index
for ty = y, y + h - 1 do
local succ = true
for tx = x, x + w - 1 do
if children[ty * #x_shares + tx + 1] == false then
succ = false
break
elseif ty == y then
end_index = ty * #x_shares + tx + 1
end
end
if not succ then
break
elseif ty > y then
end_index = ty * #x_shares + x + w
end
end
local function generate_range(s, e)
local r = {} for i = s, e do r[#r+1] = i end return r
end
local r = {
bu = children[start_index].bu, bl = children[start_index].bl,
bd = children[end_index].bd, br = children[end_index].br,
parent_id = area_index,
-- parent_cid will be filled later.
parent_x_shares = generate_range(children[start_index].parent_x_shares, children[end_index].parent_x_shares),
parent_y_shares = generate_range(children[start_index].parent_y_shares, children[end_index].parent_y_shares),
expansion = a.expansion - 1
}
merged_children[#merged_children + 1] = r
for ty = y, y + h - 1 do
local succ = true
for tx = x, x + w - 1 do
local index = ty * #x_shares + tx + 1
if index <= end_index then
children[index] = false
else
break
end
end
end
end
for i = 1, #merged_children do
a.split.children[#a.split.children + 1] = merged_children[i]
a.split.children[#a.split.children].parent_cid = #a.split.children
end
-- clean up children, remove all `false'
for i = 1, #children do
if children[i] ~= false then
a.split.children[#a.split.children + 1] = children[i]
a.split.children[#a.split.children].parent_cid = #a.split.children
end
end
push_open_areas(a.split.children)
elseif method == "s" then
if #open_areas > 0 then
local times = arg_str == "" and 1 or tonumber(arg_str)
local t = {}
local c = #open_areas
local p = open_areas[c].parent_id
while c > 0 and open_areas[c].parent_id == p do
t[#t + 1] = open_areas[c]
open_areas[c] = nil
c = c - 1
end
for i = #t, 1, -1 do
open_areas[c + 1] = t[(i + times - 1) % #t + 1]
c = c + 1
end
end
elseif method == "t" then
if #open_areas > 0 then
open_areas[#open_areas].expansion = tonumber(arg_str) or default_expansion
end
elseif method == "x" then
local a = close_area()
a.layout = arg_str
elseif method == "-" then
close_area()
elseif method == "." then
while #open_areas > 0 do
close_area()
end
elseif method == "c" then
local limit = tonumber(arg_str)
if limit == nil or limit > #open_areas then
limit = #open_areas
end
local p = open_areas[#open_areas].parent_id
while limit > 0 and open_areas[#open_areas].parent_id == p do
close_area()
limit = limit - 1
end
elseif method == "/" then
close_area().inhabitable = true
elseif method == ";" then
-- nothing
end
if #open_areas + #closed_areas > max_areas then
return nil
end
while #open_areas > 0 and open_areas[#open_areas].expansion <= 0 do
close_area()
end
arg_str = ""
return true
end
open_areas = {root}
for i = 1, #command do
local ch = command:sub(i, i)
local t = ch_info[ch]
local r = true
if t == nil then
return nil
elseif t == 3 then
if pending_op ~= nil then
r = handle_op(pending_op)
pending_op = nil
end
if #open_areas == 0 then return nil end
if arg_str == "" then
pending_op = ch
else
r = handle_op(ch)
end
elseif t == 2 or t == 1 then
if pending_op ~= nil then
handle_op(pending_op)
pending_op = nil
end
if #open_areas == 0 and t == 2 then return nil end
r = handle_op(ch)
elseif t == 0 then
arg_str = arg_str..ch
end
if not r then return nil end
end
if pending_op ~= nil then
if not handle_op(pending_op) then
return nil
end
end
if #closed_areas == 0 then
return closed_areas, open_areas, pending_op ~= nil
end
local old_closed_areas = closed_areas
closed_areas = {}
local function reorder_and_fill_adj_min(old_id)
local a = old_closed_areas[old_id]
closed_areas[#closed_areas + 1] = a
a.id = #closed_areas
if a.split then
for i = 1, #a.split.x_shares do
a.split.x_shares[i][3] = 0
a.split.x_shares[i][4] = minimum
end
for i = 1, #a.split.y_shares do
a.split.y_shares[i][3] = 0
a.split.y_shares[i][4] = minimum
end
for _, c in ipairs(a.split.children) do
if c.id then
reorder_and_fill_adj_min(c.id)
end
local x_minimum, y_minimum
if c.split then
x_minimum, y_minimum = c.x_minimum, c.y_minimum
else
x_minimum, y_minimum =
minimum, minimum
end
if type(c.parent_x_shares) == "table" then
local x_minimum_split = math.ceil(x_minimum / #c.parent_x_shares)
for i = 1, #c.parent_x_shares do
if a.split.x_shares[c.parent_x_shares[i]][4] < x_minimum_split then
a.split.x_shares[c.parent_x_shares[i]][4] = x_minimum_split
end
end
else
if a.split.x_shares[c.parent_x_shares][4] < x_minimum then
a.split.x_shares[c.parent_x_shares][4] = x_minimum
end
end
if type(c.parent_y_shares) == "table" then
local y_minimum_split = math.ceil(y_minimum / #c.parent_y_shares)
for i = 1, #c.parent_y_shares do
if a.split.y_shares[c.parent_y_shares[i]][4] < y_minimum_split then
a.split.y_shares[c.parent_y_shares[i]][4] = y_minimum_split
end
end
else
if a.split.y_shares[c.parent_y_shares][4] < y_minimum then
a.split.y_shares[c.parent_y_shares][4] = y_minimum
end
end
end
a.x_minimum = 0
a.x_total_weight = 0
for i = 1, #a.split.x_shares do
a.x_minimum = a.x_minimum + a.split.x_shares[i][4]
a.x_total_weight = a.x_total_weight + (a.split.x_shares[i][2] or 0)
end
a.y_minimum = 0
a.y_total_weight = 0
for i = 1, #a.split.y_shares do
a.y_minimum = a.y_minimum + a.split.y_shares[i][4]
a.y_total_weight = a.y_total_weight + (a.split.y_shares[i][2] or 0)
end
end
end
reorder_and_fill_adj_min(1)
-- For debugging
-- for i = 1, #closed_areas do
-- print(i, closed_areas[i].parent_id, closed_areas[i].parent_x_shares, closed_areas[i].parent_y_shares)
-- if closed_areas[i].split then
-- print("/", closed_areas[i].split.method, #closed_areas[i].split.x_shares, #closed_areas[i].split.y_shares)
-- for j = 1, #closed_areas[i].split.children do
-- print("->", closed_areas[i].split.children[j].id)
-- end
-- end
-- end
local orig_width = root.width
if root.x_minimum and root.width < root.x_minimum then
root.width = root.x_minimum
end
local orig_height = root.height
if root.y_minimum and root.height < root.y_minimum then
root.height = root.y_minimum
end
function split(id)
local a = closed_areas[id]
if a.split then
local x_shares, y_shares
x_shares, a.split.x_spare = fair_split(a.width, a.split.x_shares)
y_shares, a.split.y_spare = fair_split(a.height, a.split.y_shares)
for _, c in ipairs(a.split.children) do
if type(c.parent_x_shares) == "table" then
c.x = a.x + x_shares[c.parent_x_shares[1] - 1]
c.width = 0
for i = 1, #c.parent_x_shares do
c.width = c.width + x_shares[c.parent_x_shares[i]] - x_shares[c.parent_x_shares[i] - 1]
end
else
c.x = a.x + x_shares[c.parent_x_shares - 1]
c.width = x_shares[c.parent_x_shares] - x_shares[c.parent_x_shares - 1]
end
if type(c.parent_y_shares) == "table" then
c.y = a.y + y_shares[c.parent_y_shares[1] - 1]
c.height = 0
for i = 1, #c.parent_y_shares do
c.height = c.height + y_shares[c.parent_y_shares[i]] - y_shares[c.parent_y_shares[i] - 1]
end
else
c.y = a.y + y_shares[c.parent_y_shares - 1]
c.height = y_shares[c.parent_y_shares] - y_shares[c.parent_y_shares - 1]
end
if c.id then
split(c.id)
end
end
end
end
split(1)
-- Compatibility workaround.
if command:sub(1, 1) == "d" then
root.draft_mode = true
end
for i = 1, #closed_areas do
if closed_areas[i].x + closed_areas[i].width > root.x + orig_width or
closed_areas[i].y + closed_areas[i].height > root.y + orig_height
then
closed_areas[i].inhabitable = true
end
end
for i = 1, #open_areas do
if open_areas[i].x + open_areas[i].width > root.x + orig_width or
open_areas[i].y + open_areas[i].height > root.y + orig_height
then
open_areas[i].inhabitable = true
end
end
return closed_areas, open_areas, pending_op ~= nil
end
local function areas_to_command(areas, to_embed)
if #areas == 0 then return nil end
local function shares_to_arg_str(shares)
local arg_str = ""
for _, share in ipairs(shares) do
if #arg_str > 0 then arg_str = arg_str.."," end
arg_str = arg_str..tostring(share[1])
if not share[2] then
-- nothing
elseif share[2] > 0 then
arg_str = arg_str.."_"..tostring(share[2])
else
arg_str = arg_str.."__"..tostring(-share[2])
end
end
return arg_str
end
local function get_command(area_id)
local r
local handled_options = {}
local a = areas[area_id]
if a.hole then
return "|"
end
if a.split then
for i = 1, #a.split.children do
if a.split.children[i].hole then
a.expansion = default_expansion + 1
break
end
end
local method = a.split.method
if method == "h" then
r = shares_to_arg_str(a.split.x_shares)
r = "h"..r
elseif method == "v" then
r = shares_to_arg_str(a.split.y_shares)
r = "v"..r
elseif method == "d" or method == "w" then
local simple = true
for _, s in ipairs(a.split.x_shares) do
if s[1] ~= 1 or s[2] then simple = false break end
end
if simple then
for _, s in ipairs(a.split.y_shares) do
if s[1] ~= 1 or s[2] then simple = false break end
end
end
if method == "w" and simple then
r = tostring(#a.split.x_shares)..","..tostring(#a.split.y_shares)
else
r = shares_to_arg_str(a.split.x_shares)..",,"..shares_to_arg_str(a.split.y_shares)
method = "d"
end
local m = ""
for _, c in ipairs(a.split.children) do
if type(c.parent_x_shares) == "table" then
if #m > 0 then m = m.."," end
m = m..tostring(c.parent_x_shares[#c.parent_x_shares] - c.parent_x_shares[1] + 1)..","..
tostring(c.parent_y_shares[#c.parent_y_shares] - c.parent_y_shares[1] + 1)
end
end
if method == "d" and r == "1,,1" then
r = ""
end
if method == "d" and area_id == 1 then
r = (areas[1].draft_mode and "d" or ";d")..r
else
r = method..r..(#m == 0 and m or (method == "w" and "," or ",,"))..m
end
end
local acc_dashes = 0
for _, c in ipairs(a.split.children) do
local cr = get_command(c.id)
if cr == "-" then
acc_dashes = acc_dashes + 1
else
if acc_dashes == 0 then
elseif acc_dashes == 1 then
r = r.."-"
else
r = r.."c"..tonumber(acc_dashes)
end
acc_dashes = 0
r = r..cr
end
end
if acc_dashes > 0 then
r = r.."c"
end
elseif a.disabled then
r = "/"
elseif a.layout then
r = "x"..a.layout
else
r = "-"
end
if a.split then
if a.parent_id then
if a.expansion ~= areas[a.parent_id].expansion - 1 then
r = "t"..tostring(a.expansion)..r
end
else
if a.expansion ~= default_expansion then
r = "t"..tostring(a.expansion)..r
end
end
end
return r
end
local r = get_command(1)
if not to_embed then
r = r:gsub("[\\c]+$", ".")
end
return r
end
if not in_module then
print("Testing areas/command processing")
local function check_transcoded_command(command, expectation)
local areas, open_areas = areas_from_command(command, {x = 0, y = 0, width = 100, height = 100}, 0)
if #open_areas > 0 then
print("Found open areas after command "..command)
assert(false)
end
local transcoded = areas_to_command(areas)
if transcoded ~= expectation then
print("Mismatched transcoding for "..command..": got "..transcoded..", expected "..expectation)
assert(false)
end
end
check_transcoded_command("t.", "-")
check_transcoded_command("121h.", "h1,2,1.")
check_transcoded_command("1_10,2,1h1s131v.", "h1_10,2,1-v1,3,1.")
check_transcoded_command("332111w.", "w3,3,2,1,1,1.")
check_transcoded_command("1310111d.", ";d1,3,1,,1,1,1.")
check_transcoded_command("dw66.", "dw6,6.")
check_transcoded_command(";dw66.", ";dw6,6.")
check_transcoded_command("101dw66.", ";dw6,6.")
check_transcoded_command("3tdw66.", "t3;dw6,6.")
print("Passed.")
end
return {
areas_from_command = areas_from_command,
areas_to_command = areas_to_command,
}

View File

@ -1,3 +1,4 @@
local engine = require(... .. ".engine")
local layout = require(... .. ".layout")
local editor = require(... .. ".editor")
local switcher = require(... .. ".switcher")
@ -29,9 +30,11 @@ local function get_icon()
end
return {
engine = engine,
layout = layout,
editor = editor,
switcher = switcher,
default_name = default_name,
default_editor = default_editor,
default_layout = default_layout,
icon_raw = icon_raw,

View File

@ -1,10 +1,8 @@
local machi = {
editor = require((...):match("(.-)[^%.]+$") .. "editor"),
}
local api = {
screen = screen,
awful = require("awful"),
local this_package = ... and (...):match("(.-)[^%.]+$") or ""
local machi_editor = require(this_package.."editor")
local awful = require("awful")
local capi = {
screen = screen
}
local ERROR = 2
@ -15,106 +13,112 @@ local DEBUG = -1
local module = {
log_level = WARNING,
global_default_cmd = "dw66.",
allowing_shrinking_by_mouse_moving = false,
allow_shrinking_by_mouse_moving = false,
}
local function log(level, msg)
if level > module.log_level then
print(msg)
end
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
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
if a < b then return b else return a end
end
local function get_screen(s)
return s and api.screen[s]
return s and capi.screen[s]
end
api.awful.mouse.resize.add_enter_callback(
function (c)
c.full_width_before_move = c.width + c.border_width * 2
c.full_height_before_move = c.height + c.border_width * 2
end, 'mouse.move')
awful.mouse.resize.add_enter_callback(
function (c)
c.full_width_before_move = c.width + c.border_width * 2
c.full_height_before_move = c.height + c.border_width * 2
end, 'mouse.move')
--- find the best region for the area-like object
--- find the best area for the area-like object
-- @param c area-like object - table with properties x, y, width, and height
-- @param regions array of area-like objects
-- @return the index of the best region
local function find_region(c, regions)
local choice = 1
local choice_value = nil
local c_area = c.width * c.height
for i, a in ipairs(regions) do
local x_cap = max(0, min(c.x + c.width, a.x + a.width) - max(c.x, a.x))
local y_cap = max(0, min(c.y + c.height, a.y + a.height) - max(c.y, a.y))
local cap = x_cap * y_cap
-- -- a cap b / a cup b
-- local cup = c_area + a.width * a.height - cap
-- if cup > 0 then
-- local itx_ratio = cap / cup
-- if choice_value == nil or choice_value < itx_ratio then
-- choice_value = itx_ratio
-- choice = i
-- end
-- end
-- a cap b
if choice_value == nil or choice_value < cap then
choice = i
choice_value = cap
end
end
return choice
-- @param areas array of area objects
-- @return the index of the best area
local function find_area(c, areas)
local choice = 1
local choice_value = nil
local c_area = c.width * c.height
for i, a in ipairs(areas) do
if not a.inhabitable then
local x_cap = max(0, min(c.x + c.width, a.x + a.width) - max(c.x, a.x))
local y_cap = max(0, min(c.y + c.height, a.y + a.height) - max(c.y, a.y))
local cap = x_cap * y_cap
-- -- a cap b / a cup b
-- local cup = c_area + a.width * a.height - cap
-- if cup > 0 then
-- local itx_ratio = cap / cup
-- if choice_value == nil or choice_value < itx_ratio then
-- choice_value = itx_ratio
-- choice = i
-- end
-- end
-- a cap b
if choice_value == nil or choice_value < cap then
choice = i
choice_value = cap
end
end
end
return choice
end
local function distance(x1, y1, x2, y2)
-- use d1
return math.abs(x1 - x2) + math.abs(y1 - y2)
-- use d1
return math.abs(x1 - x2) + math.abs(y1 - y2)
end
local function find_lu(c, regions, rd)
local lu = nil
for i, a in ipairs(regions) do
if rd == nil or (a.x < regions[rd].x + regions[rd].width and a.y < regions[rd].y + regions[rd].height) then
if lu == nil or distance(c.x, c.y, a.x, a.y) < distance(c.x, c.y, regions[lu].x, regions[lu].y) then
lu = i
end
end
end
return lu
local function find_lu(c, areas, rd)
local lu = nil
for i, a in ipairs(areas) do
if not a.inhabitable then
if rd == nil or (a.x < areas[rd].x + areas[rd].width and a.y < areas[rd].y + areas[rd].height) then
if lu == nil or distance(c.x, c.y, a.x, a.y) < distance(c.x, c.y, areas[lu].x, areas[lu].y) then
lu = i
end
end
end
end
return lu
end
local function find_rd(c, regions, lu)
local x, y
x = c.x + c.width + (c.border_width or 0)
y = c.y + c.height + (c.border_width or 0)
local rd = nil
for i, a in ipairs(regions) do
if lu == nil or (a.x + a.width > regions[lu].x and a.y + a.height > regions[lu].y) then
if rd == nil or distance(x, y, a.x + a.width, a.y + a.height) < distance(x, y, regions[rd].x + regions[rd].width, regions[rd].y + regions[rd].height) then
rd = i
end
end
end
return rd
local function find_rd(c, areas, lu)
local x, y
x = c.x + c.width + (c.border_width or 0)
y = c.y + c.height + (c.border_width or 0)
local rd = nil
for i, a in ipairs(areas) do
if not a.inhabitable then
if lu == nil or (a.x + a.width > areas[lu].x and a.y + a.height > areas[lu].y) then
if rd == nil or distance(x, y, a.x + a.width, a.y + a.height) < distance(x, y, areas[rd].x + areas[rd].width, areas[rd].y + areas[rd].height) then
rd = i
end
end
end
end
return rd
end
function module.set_geometry(c, region_lu, region_rd, useless_gap, border_width)
-- We try to negate the gap of outer layer
if region_lu ~= nil then
c.x = region_lu.x - useless_gap
c.y = region_lu.y - useless_gap
end
function module.set_geometry(c, area_lu, area_rd, useless_gap, border_width)
-- We try to negate the gap of outer layer
if area_lu ~= nil then
c.x = area_lu.x - useless_gap
c.y = area_lu.y - useless_gap
end
if region_rd ~= nil then
c.width = region_rd.x + region_rd.width - c.x + useless_gap - border_width * 2
c.height = region_rd.y + region_rd.height - c.y + useless_gap - border_width * 2
end
if area_rd ~= nil then
c.width = area_rd.x + area_rd.width - c.x + useless_gap - border_width * 2
c.height = area_rd.y + area_rd.height - c.y + useless_gap - border_width * 2
end
end
function module.create(args_or_name, editor, default_cmd)
@ -132,7 +136,15 @@ function module.create(args_or_name, editor, default_cmd)
else
return nil
end
args.editor = args.editor or editor or machi.editor.default_editor
args.name = args.name or function (tag)
if tag.machi_name_cache == nil then
tag.machi_name_cache =
tostring(tag.screen.geometry.width) .. "x" .. tostring(tag.screen.geometry.height) .. "+" ..
tostring(tag.screen.geometry.x) .. "+" .. tostring(tag.screen.geometry.y) .. '+' .. tag.name
end
return tag.machi_name_cache
end
args.editor = args.editor or editor or machi_editor.default_editor
args.default_cmd = args.default_cmd or default_cmd or global_default_cmd
args.persistent = args.persistent == nil or args.persistent
@ -143,253 +155,259 @@ function module.create(args_or_name, editor, default_cmd)
return (args.name_func and args.name_func(tag) or args.name), args.persistent
end
local function get_instance_(tag)
local name, persistent = get_instance_info(tag)
if instances[name] == nil then
instances[name] = {
layout = layout,
cmd = persistent and args.editor.get_last_cmd(name) or nil,
regions_cache = {},
tag_data = {},
}
if instances[name].cmd == nil then
instances[name].cmd = args.default_cmd
end
end
return instances[name]
end
local function get_regions(workarea, tag)
local instance = get_instance_(tag)
local cmd = instance.cmd or module.global_default_cmd
if cmd == nil then return {}, false end
local key = tostring(workarea.width) .. "x" .. tostring(workarea.height) .. "+" .. tostring(workarea.x) .. "+" .. tostring(workarea.y)
if instance.regions_cache[key] == nil then
instance.regions_cache[key] = args.editor.run_cmd(workarea, cmd)
end
return instance.regions_cache[key], cmd:sub(1,1) == "d"
end
local function set_cmd(cmd, tag)
local instance = get_instance_(tag)
if instance.cmd ~= cmd then
instance.cmd = cmd
instance.regions_cache = {}
instance.tag_data = {}
end
end
local function arrange(p)
local useless_gap = p.useless_gap
local wa = get_screen(p.screen).workarea -- get the real workarea without the gap (instead of p.workarea)
local cls = p.clients
local tag = get_screen(p.screen).selected_tag
local instance = get_instance_(tag)
local regions, draft_mode = get_regions(wa, tag)
if #regions == 0 then return end
local nested_clients = {}
for _, c in ipairs(cls) do
if c.machi == nil then
c.machi = setmetatable({}, {__mode = "v"})
end
end
if draft_mode then
for i, c in ipairs(cls) do
if c.floating or c.immobilized then
log(DEBUG, "Ignore client " .. tostring(c))
else
local skip = false
if c.machi.lu ~= nil and c.machi.rd ~= nil and
c.machi.lu <= #regions and c.machi.rd <= #regions
then
if regions[c.machi.lu].x == c.x and
regions[c.machi.lu].y == c.y and
regions[c.machi.rd].x + regions[c.machi.rd].width - c.border_width * 2 == c.x + c.width and
regions[c.machi.rd].y + regions[c.machi.rd].height - c.border_width * 2 == c.y + c.height
then
skip = true
end
end
local lu = nil
local rd = nil
if not skip then
log(DEBUG, "Compute regions for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
lu = find_lu(c, regions)
if lu ~= nil then
c.x = regions[lu].x
c.y = regions[lu].y
rd = find_rd(c, regions, lu)
end
end
if lu ~= nil and rd ~= nil then
c.machi.instance = instance
c.machi.region, c.machi.lu, c.machi.rd = nil, lu, rd
p.geometries[c] = {}
module.set_geometry(p.geometries[c], regions[lu], regions[rd], useless_gap, 0)
end
local function get_instance_(tag)
local name, persistent = get_instance_info(tag)
if instances[name] == nil then
instances[name] = {
layout = layout,
cmd = persistent and args.editor.get_last_cmd(name) or nil,
areas_cache = {},
tag_data = {},
}
if instances[name].cmd == nil then
instances[name].cmd = args.default_cmd
end
end
else
for i, c in ipairs(cls) do
if c.floating or c.immobilized then
log(DEBUG, "Ignore client " .. tostring(c))
else
if c.machi.region ~= nil and
regions[c.machi.region].layout == nil and
regions[c.machi.region].x == c.x and
regions[c.machi.region].y == c.y and
regions[c.machi.region].width - c.border_width * 2 == c.width and
regions[c.machi.region].height - c.border_width * 2 == c.height
then
else
log(DEBUG, "Compute regions for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
local region = find_region(c, regions)
c.machi.instance = instance
c.machi.region, c.machi.lu, c.machi.rd = region, nil, nil
p.geometries[c] = {}
if regions[region].layout ~= nil then
local clients = nested_clients[region]
if clients == nil then clients = {}; nested_clients[region] = clients end
clients[#clients + 1] = c
else
module.set_geometry(p.geometries[c], regions[region], regions[region], useless_gap, 0)
end
end
end
return instances[name]
end
local function get_areas(screen, tag)
local workarea = screen.workarea
local instance = get_instance_(tag)
local cmd = instance.cmd or module.global_default_cmd
if cmd == nil then return {}, false end
local key = tostring(workarea.width) .. "x" .. tostring(workarea.height) .. "+" .. tostring(workarea.x) .. "+" .. tostring(workarea.y)
if instance.areas_cache[key] == nil then
instance.areas_cache[key] = args.editor.run_cmd(cmd, screen, tag)
end
local draft_mode = instance.areas_cache[key] and instance.areas_cache[key][1].draft_mode
return instance.areas_cache[key], draft_mode
end
local function set_cmd(cmd, tag)
local instance = get_instance_(tag)
if instance.cmd ~= cmd then
instance.cmd = cmd
instance.areas_cache = {}
instance.tag_data = {}
end
end
local function arrange(p)
local useless_gap = p.useless_gap
local screen = get_screen(p.screen)
local wa = screen.workarea -- get the real workarea without the gap (instead of p.workarea)
local cls = p.clients
local tag = screen.selected_tag
local instance = get_instance_(tag)
local areas, draft_mode = get_areas(screen, tag)
if #areas == 0 then return end
local nested_clients = {}
for _, c in ipairs(cls) do
if c.machi == nil then
c.machi = setmetatable({}, {__mode = "v"})
end
end
end
for region, clients in pairs(nested_clients) do
if instance.tag_data[region] == nil then
-- TODO: Make the default more flexible.
instance.tag_data[region] = {
column_count = 1,
master_count = 1,
master_fill_policy = "expand",
useless_gap = 0,
master_width_factor = 0.5,
_private = {
awful_tag_properties = {
},
},
}
end
local nested_params = {
tag = instance.tag_data[region],
screen = p.screen,
clients = clients,
padding = 0,
geometry = {
x = regions[region].x,
y = regions[region].y,
width = regions[region].width,
height = regions[region].height,
},
-- Not sure how useless_gap adjustment works here. It seems to work anyway.
workarea = {
x = regions[region].x - useless_gap,
y = regions[region].y - useless_gap,
width = regions[region].width + useless_gap * 2,
height = regions[region].height + useless_gap * 2,
},
useless_gap = useless_gap,
geometries = {},
}
regions[region].layout.arrange(nested_params)
for _, c in ipairs(clients) do
p.geometries[c] = {
x = nested_params.geometries[c].x,
y = nested_params.geometries[c].y,
width = nested_params.geometries[c].width,
height = nested_params.geometries[c].height,
}
end
end
end
end
if draft_mode then
for i, c in ipairs(cls) do
if c.floating or c.immobilized then
log(DEBUG, "Ignore client " .. tostring(c))
else
local skip = false
if c.machi.lu ~= nil and c.machi.rd ~= nil and
c.machi.lu <= #areas and c.machi.rd <= #areas and
not areas[c.machi.lu].inhabitable and not areas[c.machi.rd].inhabitable
then
if areas[c.machi.lu].x == c.x and
areas[c.machi.lu].y == c.y and
areas[c.machi.rd].x + areas[c.machi.rd].width - c.border_width * 2 == c.x + c.width and
areas[c.machi.rd].y + areas[c.machi.rd].height - c.border_width * 2 == c.y + c.height
then
skip = true
end
end
local function resize_handler (c, context, h)
local workarea = c.screen.workarea
local regions, draft_mode = get_regions(workarea, c.screen.selected_tag)
local lu = nil
local rd = nil
if not skip then
log(DEBUG, "Compute areas for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
lu = find_lu(c, areas)
if lu ~= nil then
c.x = areas[lu].x
c.y = areas[lu].y
rd = find_rd(c, areas, lu)
end
end
if #regions == 0 then return end
if draft_mode then
local lu = find_lu(h, regions)
local rd = nil
if lu ~= nil then
if context == "mouse.move" then
-- Use the initial width and height since it may change in undesired way.
local hh = {}
hh.x = regions[lu].x
hh.y = regions[lu].y
hh.width = c.full_width_before_move
hh.height = c.full_height_before_move
rd = find_rd(hh, regions, lu)
if rd ~= nil and not module.allowing_shrinking_by_mouse_moving and
(regions[rd].x + regions[rd].width - regions[lu].x < c.full_width_before_move or
regions[rd].y + regions[rd].height - regions[lu].y < c.full_height_before_move) then
hh.x = regions[rd].x + regions[rd].width - c.full_width_before_move
hh.y = regions[rd].y + regions[rd].height - c.full_height_before_move
lu = find_lu(hh, regions, rd)
end
else
local hh = {}
hh.x = h.x
hh.y = h.y
hh.width = h.width
hh.height = h.height
hh.border_width = c.border_width
rd = find_rd(hh, regions, lu)
if lu ~= nil and rd ~= nil then
c.machi.instance = instance
c.machi.area, c.machi.lu, c.machi.rd = nil, lu, rd
p.geometries[c] = {}
module.set_geometry(p.geometries[c], areas[lu], areas[rd], useless_gap, 0)
end
end
end
else
for i, c in ipairs(cls) do
if c.floating or c.immobilized then
log(DEBUG, "Ignore client " .. tostring(c))
else
if c.machi.area ~= nil and
c.machi.area < #areas and
not areas[c.machi.area].inhabitable and
areas[c.machi.area].layout == nil and
areas[c.machi.area].x == c.x and
areas[c.machi.area].y == c.y and
areas[c.machi.area].width - c.border_width * 2 == c.width and
areas[c.machi.area].height - c.border_width * 2 == c.height
then
else
log(DEBUG, "Compute areas for " .. (c.name or ("<untitled:" .. tostring(c) .. ">")))
local area = find_area(c, areas)
c.machi.instance = instance
c.machi.area, c.machi.lu, c.machi.rd = area, nil, nil
p.geometries[c] = {}
if machi_editor.nested_layouts[areas[area].layout] ~= nil then
local clients = nested_clients[area]
if clients == nil then clients = {}; nested_clients[area] = clients end
clients[#clients + 1] = c
else
module.set_geometry(p.geometries[c], areas[area], areas[area], useless_gap, 0)
end
end
end
end
if lu ~= nil and rd ~= nil then
c.machi.lu = lu
c.machi.rd = rd
module.set_geometry(c, regions[lu], regions[rd], 0, c.border_width)
for area, clients in pairs(nested_clients) do
if instance.tag_data[area] == nil then
-- TODO: Make the default more flexible.
instance.tag_data[area] = {
column_count = 1,
master_count = 1,
master_fill_policy = "expand",
gap = 0,
master_width_factor = 0.5,
_private = {
awful_tag_properties = {
},
},
}
end
local nested_params = {
tag = instance.tag_data[area],
screen = p.screen,
clients = clients,
padding = 0,
geometry = {
x = areas[area].x,
y = areas[area].y,
width = areas[area].width,
height = areas[area].height,
},
-- Not sure how useless_gap adjustment works here. It seems to work anyway.
workarea = {
x = areas[area].x - useless_gap,
y = areas[area].y - useless_gap,
width = areas[area].width + useless_gap * 2,
height = areas[area].height + useless_gap * 2,
},
useless_gap = useless_gap,
geometries = {},
}
machi_editor.nested_layouts[areas[area].layout].arrange(nested_params)
for _, c in ipairs(clients) do
p.geometries[c] = {
x = nested_params.geometries[c].x,
y = nested_params.geometries[c].y,
width = nested_params.geometries[c].width,
height = nested_params.geometries[c].height,
}
end
end
end
else
if context ~= "mouse.move" then return end
end
end
if #regions == 0 then return end
local function resize_handler (c, context, h)
local areas, draft_mode = get_areas(c.screen, c.screen.selected_tag)
local center_x = h.x + h.width / 2
local center_y = h.y + h.height / 2
if #areas == 0 then return end
local choice = 1
local choice_value = nil
if draft_mode then
local lu = find_lu(h, areas)
local rd = nil
if lu ~= nil then
if context == "mouse.move" then
-- Use the initial width and height since it may change in undesired way.
local hh = {}
hh.x = areas[lu].x
hh.y = areas[lu].y
hh.width = c.full_width_before_move
hh.height = c.full_height_before_move
rd = find_rd(hh, areas, lu)
for i, r in ipairs(regions) do
local r_x = r.x + r.width / 2
local r_y = r.y + r.height / 2
local dis = (r_x - center_x) * (r_x - center_x) + (r_y - center_y) * (r_y - center_y)
if choice_value == nil or choice_value > dis then
choice = i
choice_value = dis
if rd ~= nil and not module.allowing_shrinking_by_mouse_moving and
(areas[rd].x + areas[rd].width - areas[lu].x < c.full_width_before_move or
areas[rd].y + areas[rd].height - areas[lu].y < c.full_height_before_move) then
hh.x = areas[rd].x + areas[rd].width - c.full_width_before_move
hh.y = areas[rd].y + areas[rd].height - c.full_height_before_move
lu = find_lu(hh, areas, rd)
end
else
local hh = {}
hh.x = h.x
hh.y = h.y
hh.width = h.width
hh.height = h.height
hh.border_width = c.border_width
rd = find_rd(hh, areas, lu)
end
if lu ~= nil and rd ~= nil then
c.machi.lu = lu
c.machi.rd = rd
module.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
end
end
end
else
if context ~= "mouse.move" then return end
if c.machi.region ~= choice then
c.machi.region = choice
module.set_geometry(c, regions[choice], regions[choice], 0, c.border_width)
end
end
end
if #areas == 0 then return end
layout.name = "machi"
layout.arrange = arrange
layout.resize_handler = resize_handler
layout.machi_get_instance_info = get_instance_info
layout.machi_set_cmd = set_cmd
layout.machi_get_regions = get_regions
return layout
local center_x = h.x + h.width / 2
local center_y = h.y + h.height / 2
local choice = 1
local choice_value = nil
for i, r in ipairs(areas) do
local r_x = r.x + r.width / 2
local r_y = r.y + r.height / 2
local dis = (r_x - center_x) * (r_x - center_x) + (r_y - center_y) * (r_y - center_y)
if choice_value == nil or choice_value > dis then
choice = i
choice_value = dis
end
end
if c.machi.area ~= choice then
c.machi.area = choice
module.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
end
end
end
layout.name = args.icon_name or "machi"
layout.editor = args.editor
layout.arrange = arrange
layout.resize_handler = resize_handler
layout.machi_get_instance_info = get_instance_info
layout.machi_set_cmd = set_cmd
layout.machi_get_areas = get_areas
return layout
end
return module

View File

@ -1,5 +1,6 @@
local machi = {
layout = require((...):match("(.-)[^%.]+$") .. "layout"),
layout = require((...):match("(.-)[^%.]+$") .. "layout"),
engine = require((...):match("(.-)[^%.]+$") .. "engine"),
}
local api = {
@ -15,6 +16,8 @@ local api = {
dpi = require("beautiful.xresources").apply_dpi,
}
local gtimer = require("gears.timer")
local ERROR = 2
local WARNING = 1
local INFO = 0
@ -53,6 +56,9 @@ function module.start(c, exit_keys)
local border_color = with_alpha(api.gears.color(
api.beautiful.machi_switcher_border_color or api.beautiful.border_focus),
api.beautiful.machi_switcher_border_opacity or 0.25)
local border_color_hl = with_alpha(api.gears.color(
api.beautiful.machi_switcher_border_hl_color or api.beautiful.border_focus),
api.beautiful.machi_switcher_border_hl_opacity or 0.75)
local fill_color = with_alpha(api.gears.color(
api.beautiful.machi_switcher_fill_color or api.beautiful.bg_normal),
api.beautiful.machi_switcher_fill_opacity or 0.25)
@ -67,14 +73,16 @@ function module.start(c, exit_keys)
local traverse_radius = api.dpi(5)
local screen = c and c.screen or api.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
local layout = api.layout.get(screen)
if (c ~= nil and c.floating) or layout.machi_get_regions == nil then return end
if (c ~= nil and c.floating) or layout.machi_get_areas == nil then return end
local regions, draft_mode = layout.machi_get_regions(screen.workarea, screen.selected_tag)
if regions == nil or #regions == 0 then
local areas, draft_mode = layout.machi_get_areas(screen, screen.selected_tag)
if areas == nil or #areas == 0 then
return
end
@ -91,7 +99,6 @@ function module.start(c, exit_keys)
})
infobox.visible = true
local tablist_region = nil
local tablist = nil
local tablist_index = nil
@ -104,23 +111,35 @@ function module.start(c, exit_keys)
traverse_y = screen.workarea.y + screen.workarea.height / 2
end
local selected_area_ = nil
local function selected_area()
if selected_area_ == nil then
for i, a in ipairs(areas) do
if not a.inhabitable and
a.x <= traverse_x and traverse_x < a.x + a.width and
a.y <= traverse_y and traverse_y < a.y + a.height
then
selected_area_ = i
end
end
end
return selected_area_
end
local function set_selected_area(a)
selected_area_ = a
end
local function maintain_tablist()
if tablist == nil then
tablist = {}
for i, a in ipairs(regions) do
if a.x <= traverse_x and traverse_x < a.x + a.width and
a.y <= traverse_y and traverse_y < a.y + a.height
then
active_region = i
end
end
local active_area = selected_area()
for _, tc in ipairs(screen.tiled_clients) do
if not (tc.floating or tc.immobilized)
then
if regions[active_region].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= regions[active_region].x + regions[active_region].width and
regions[active_region].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= regions[active_region].y + regions[active_region].height
if areas[active_area].x <= tc.x + tc.width + tc.border_width * 2 and tc.x <= areas[active_area].x + areas[active_area].width and
areas[active_area].y <= tc.y + tc.height + tc.border_width * 2 and tc.y <= areas[active_area].y + areas[active_area].height
then
tablist[#tablist + 1] = tc
end
@ -159,30 +178,25 @@ function module.start(c, exit_keys)
cr:rectangle(0, 0, width, height)
cr:fill()
local msg, ext, active_region
for i, a in ipairs(regions) do
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
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(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()
-- TODO deduplicate this with code in maintain_tablist()
if a.x <= traverse_x and traverse_x < a.x + a.width and
a.y <= traverse_y and traverse_y < a.y + a.height
then
active_region = i
local msg, ext
local active_area = selected_area()
for i, a in ipairs(areas) do
if not a.inhabitable or i == active_area then
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
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 #tablist > 0 then
local a = regions[active_region]
local a = areas[active_area]
local pl = api.lgi.Pango.Layout.create(cr)
pl:set_font_description(tablist_font_desc)
@ -192,7 +206,7 @@ function module.start(c, exit_keys)
local exts = {}
for index, tc in ipairs(tablist) do
local label = tc.name
local label = tc.name or "<unnamed>"
pl:set_text(label)
local w, h
w, h = pl:get_size()
@ -208,7 +222,7 @@ function module.start(c, exit_keys)
local y_offset = a.y + a.height / 2 - list_height / 2 + vpadding - start_y
-- cr:rectangle(a.x - start_x, y_offset - vpadding - start_y, a.width, list_height)
-- cover the entire region
-- cover the entire area
cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height)
cr:set_source(fill_color)
cr:fill()
@ -218,7 +232,7 @@ function module.start(c, exit_keys)
cr:fill()
for index, tc in ipairs(tablist) do
local label = tc.name
local label = tc.name or "<unnamed>"
local ext = exts[index]
if index == tablist_index then
cr:rectangle(x_offset - ext.width / 2 - vpadding / 2, y_offset - vpadding / 2, ext.width + vpadding, ext.height + vpadding)
@ -298,37 +312,28 @@ function module.start(c, exit_keys)
end
end
local current_region = nil
local current_area = selected_area()
if c and (shift or ctrl) then
for i, a in ipairs(regions) do
if a.x <= traverse_x and traverse_x < a.x + a.width and
a.y <= traverse_y and traverse_y < a.y + a.height
then
current_region = i
break
end
end
if shift then
if current_region == nil or
regions[current_region].x ~= c.x or
regions[current_region].y ~= c.y
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
current_region = nil
set_selected_area(nil)
end
elseif ctrl then
local ex = c.x + c.width + c.border_width * 2
local ey = c.y + c.height + c.border_width * 2
if current_region == nil or
regions[current_region].x + regions[current_region].width ~= ex or
regions[current_region].y + regions[current_region].height ~= ey
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
current_region = nil
set_selected_area(nil)
end
end
end
@ -336,12 +341,10 @@ function module.start(c, exit_keys)
local choice = nil
local choice_value
for i, a in ipairs(regions) do
if a.x <= traverse_x and traverse_x < a.x + a.width and
a.y <= traverse_y and traverse_y < a.y + a.height
then
current_region = i
end
current_area = selected_area()
for i, a in ipairs(areas) do
if a.inhabitable then goto continue end
local v
if key == "Up" then
@ -378,10 +381,11 @@ function module.start(c, exit_keys)
choice = i
choice_value = v
end
::continue::
end
if choice == nil then
choice = current_region
choice = current_area
if key == "Up" then
traverse_y = screen.workarea.y
elseif key == "Down" then
@ -394,9 +398,10 @@ function module.start(c, exit_keys)
end
if choice ~= nil then
traverse_x = max(regions[choice].x + traverse_radius, min(regions[choice].x + regions[choice].width - traverse_radius, traverse_x))
traverse_y = max(regions[choice].y + traverse_radius, min(regions[choice].y + regions[choice].height - traverse_radius, traverse_y))
traverse_x = max(areas[choice].x + traverse_radius, min(areas[choice].x + areas[choice].width - traverse_radius, traverse_x))
traverse_y = max(areas[choice].y + traverse_radius, min(areas[choice].y + areas[choice].height - traverse_radius, traverse_y))
tablist = nil
set_selected_area(nil)
if c and ctrl and draft_mode then
local lu = c.machi.lu
@ -404,28 +409,28 @@ function module.start(c, exit_keys)
if shift then
lu = choice
if regions[rd].x + regions[rd].width <= regions[lu].x or
regions[rd].y + regions[rd].height <= regions[lu].y
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 regions[rd].x + regions[rd].width <= regions[lu].x or
regions[rd].y + regions[rd].height <= regions[lu].y
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, regions[lu], regions[rd], 0, c.border_width)
machi.layout.set_geometry(c, areas[lu], areas[rd], 0, c.border_width)
elseif lu ~= nil then
machi.layout.set_geometry(c, regions[lu], nil, 0, c.border_width)
machi.layout.set_geometry(c, areas[lu], nil, 0, c.border_width)
elseif rd ~= nil then
c.x = min(c.x, regions[rd].x)
c.y = min(c.y, regions[rd].y)
machi.layout.set_geometry(c, nil, regions[rd], 0, c.border_width)
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
c.machi.lu = lu
c.machi.rd = rd
@ -436,11 +441,11 @@ function module.start(c, exit_keys)
elseif c and shift then
-- move the window
if draft_mode then
c.x = regions[choice].x
c.y = regions[choice].y
c.x = areas[choice].x
c.y = areas[choice].y
else
machi.layout.set_geometry(c, regions[choice], regions[choice], 0, c.border_width)
c.machi.region = choice
machi.layout.set_geometry(c, areas[choice], areas[choice], 0, c.border_width)
c.machi.area = choice
end
c:emit_signal("request::activate", "mouse.move", {raise=false})
c:raise()
@ -458,6 +463,42 @@ function module.start(c, exit_keys)
infobox.bgimage = draw_info
end
elseif (key == "u" or key == "Prior") and not draft_mode then
local current_area = selected_area()
if areas[current_area].parent_id then
tablist = nil
set_selected_area(areas[current_area].parent_id)
infobox.bgimage = draw_info
end
elseif key == "/" and not draft_mode then
local current_area = selected_area()
areas[current_area].hole = true
local prefix, suffix = machi.engine.areas_to_command(
areas, true):match("(.*)|(.*)")
print(prefix, suffix)
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 ()
print(layout.editor)
layout.editor.start_interactive(
screen,
{
workarea = workarea,
cmd_prefix = prefix,
cmd_suffix = suffix,
}
)
end
)
exit()
elseif key == "Escape" or key == "Return" then
exit()
else