big refactoring; advanced grid mode; readme

This commit is contained in:
Xinhao Yuan 2019-08-10 12:25:51 -04:00
parent 820dcc98f6
commit c8d2b5fd45
3 changed files with 319 additions and 122 deletions

127
README.md
View File

@ -23,7 +23,7 @@ Please let me know if it does not work in other versions.
Suppose this git is checked out at `~/.config/awesome/layout-machi` Suppose this git is checked out at `~/.config/awesome/layout-machi`
`machi = require("layout-machi")` Use `local machi = require("layout-machi")` to load the module.
The package provide a default layout `machi.default_layout` and editor `machi.default_editor`, which can be added into the layout list. The package provide a default layout `machi.default_layout` and editor `machi.default_editor`, which can be added into the layout list.
@ -33,7 +33,7 @@ The package comes with the icon for `layoutbox`, which can be set with the follo
## Use the layout ## Use the layout
Use `layout = machi.layout.create(name, editor)` to instantiate the layout with an editor object. Use `local layout = machi.layout.create(name, editor)` to instantiate the layout with an editor object.
`name` can be a string or a function returning a string (see `init.lua` and "Advanced" below). `name` can be a string or a function returning a string (see `init.lua` and "Advanced" below).
This is used for having different actual layout dependent on tags. This is used for having different actual layout dependent on tags.
@ -42,25 +42,46 @@ This is used for having different actual layout dependent on tags.
`machi.default_editor` can be used, or see below on creating editors. `machi.default_editor` can be used, or see below on creating editors.
You can create multiple layouts with different names and share the same editor. You can create multiple layouts with different names and share the same editor.
## Editor ## The layout editor and commands
Call `editor = machi.editor.create()` to create an 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 = awful.screen.focused(), l = awful.layout.get(s))`. To edit the layout `l` on screen `s`, call `editor.start_interactive(s = awful.screen.focused(), l = awful.layout.get(s))`.
### The layout editing command ### Basic usage
The editing starts with the open area of the entire workarea, takes commands to split the current area into multiple sub-areas, then recursively edits each of them. 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.
The editor is keyboard driven, each command is a key with optional digits (namely `D`) before it as parameter (or multiple parameters depending on the command). 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 sequence 1. `Up`/`Down`: restore to the history command
2. `h`/`v`: split the current region horizontally/vertically into `#D` regions. The split will respect the ratio of digits in `D`. 2. `Backspace`: undo the last command.
3. `w`: Take the last two digits from `D` as `D = ...AB` (1 if `D` is shorter than 2 digits), and split the current region equally into A rows and B columns. If no digits are provided at all, behave the same as `Space`. 3. `Escape`: exit the editor without saving the layout.
4. `d`: Take the argument in the format of `A0B`, where `A` and `B` do not contain any `0`, apply `h` with argument `A` unless `A` is shorter than 2 digits. On each splitted region, apply `v` with argument `B` unless `B` is shorter than 2 digit. Does nothing if the argument is ill-formed. 4. `Enter`: when all regions are defined, hit enter will save the layout.
5. `s`: shift the current editing region with other open regions. If digits are provided, shift for that many times.
6. `Space` or `-`: Without parameters, close the current region and move to the next open region. With digits, set the maximum depth of splitting (the default depth is 2). ### Layout command
7. `Enter`/`.`: close all open regions. When all regions are closed, press `Enter` will save the layout and exit the editor.
8. `Backspace`: undo the last command. As aforementioned, command a sequence of operations.
9. `Escape`: exit the editor without saving the layout. 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)
2. Operations taking argument string as a single number.
`s` (shifting active region)
3. Operation not taking argument.
`.` (Finish all regions), `-` (Finish the current region), `;` (No-op)
Argument string 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.
Each operation may take argument string either from before (such as `22w`) or after (such as `w22`).
When any ambiguity arises, operation before always take the argument after. So `h11v` is interpreted as `h11` and `v`.
For examples: For examples:
@ -125,6 +146,76 @@ Tada!
99 AAAA BBBB CC 99 AAAA BBBB CC
``` ```
### Advanced grid layout
__More document coming soon. For now there is only a running example.__
Simple grid, `w44`:
```
0 1 2 3
4 5 6 7
8 9 A B
C D E F
```
Merge grid from the top-left corner, size 3x1, `w4431`:
```
0-0-0 1
2 3 4 5
6 7 8 9
A B C D
```
Another merge, size 1x3, `w443113`:
```
0-0-0 1
|
2 3 4 1
|
5 6 7 1
8 9 A B
```
Another merge, size 1x3, `w44311313`:
```
0-0-0 1
|
2 3 4 1
| |
2 5 6 1
|
2 7 8 9
```
Another merge, size 2x2, `w4431131322`:
```
0-0-0 1
|
2 3-3 1
| | | |
2 3-3 1
|
2 4 5 6
```
Final merge, size 3x1, `w443113132231`:
```
0-0-0 1
|
2 3-3 1
| | | |
2 3-3 1
|
2 4-4-4
```
### Draft mode ### Draft mode
__This mode is experimental. Its usage may change fast.__ __This mode is experimental. Its usage may change fast.__
@ -133,8 +224,8 @@ Unlike the original machi layout, where a window fits in a single region, draft
Each tiled window is associated with a upper-left region (ULR) and a bottom-right region (BRR). 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. The geometry of the window is from the upper-left corner of the ULR to the bottom-right corner of the BRR.
This is suppose to work with regions produced with `d` command. This is suppose to work with regions 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, `d12210121d`. To enable draft mode in a layout, configure the layout with a command with a leading `d`, for example, `d12210121`, or `dw66`.
### Persistent history ### Persistent history

View File

@ -201,20 +201,23 @@ local function create(data)
max_depth = init_max_depth max_depth = init_max_depth
current_info = "" current_info = ""
current_cmd = "" current_cmd = ""
pending_op = nil
to_exit = false to_exit = false
to_apply = false to_apply = false
end end
local function push_history() local function push_history()
history[#history + 1] = {#closed_areas, #open_areas, {}, current_info, current_cmd, max_depth, arg_str} if history == nil then return end
history[#history + 1] = {#closed_areas, #open_areas, {}, current_info, current_cmd, pending_op, max_depth, arg_str}
end end
local function discard_history() local function discard_history()
if history == nil then return end
table.remove(history, #history) table.remove(history, #history)
end end
local function pop_history() local function pop_history()
if #history == 0 then return end if history == nil or #history == 0 then return end
for i = history[#history][1] + 1, #closed_areas do for i = history[#history][1] + 1, #closed_areas do
table.remove(closed_areas, #closed_areas) table.remove(closed_areas, #closed_areas)
end end
@ -229,8 +232,9 @@ local function create(data)
current_info = history[#history][4] current_info = history[#history][4]
current_cmd = history[#history][5] current_cmd = history[#history][5]
max_depth = history[#history][6] pending_op = history[#history][6]
arg_str = history[#history][7] max_depth = history[#history][7]
arg_str = history[#history][8]
table.remove(history, #history) table.remove(history, #history)
end end
@ -238,6 +242,8 @@ local function create(data)
local function pop_open_area() local function pop_open_area()
local a = open_areas[#open_areas] local a = open_areas[#open_areas]
table.remove(open_areas, #open_areas) table.remove(open_areas, #open_areas)
if history == nil or #history == 0 then return a end
local idx = history[#history][2] - #open_areas local idx = history[#history][2] - #open_areas
-- only save when the position has been firstly poped -- only save when the position has been firstly poped
if idx > #history[#history][3] then if idx > #history[#history][3] then
@ -246,17 +252,38 @@ local function create(data)
return a return a
end end
local split_count = 0 local function push_area()
closed_areas[#closed_areas + 1] = pop_open_area()
end
local function handle_split(method, alt) local function push_children(c)
split_count = split_count + 1 for i = #c, 1, -1 do
if c[i] ~= false then
if c[i].x ~= math.floor(c[i].x)
or c[i].y ~= math.floor(c[i].y)
or c[i].width ~= math.floor(c[i].width)
or c[i].height ~= math.floor(c[i].height)
then
print("warning, splitting yields floating area " .. _area_tostring(c[i]))
end
open_areas[#open_areas + 1] = c[i]
end
end
end
local a = pop_open_area() local op_count = 0
print("split " .. method .. " " .. tostring(alt) .. " " .. arg_str .. " " .. _area_tostring(a)) local function handle_op(method)
op_count = op_count + 1
local l = method:lower()
local alt = method ~= l
method = l
print("op " .. method .. " " .. tostring(alt) .. " " .. arg_str)
if method == "h" or method == "v" then if method == "h" or method == "v" then
local a = pop_open_area()
local args = parse_arg_string(arg_str, 0) local args = parse_arg_string(arg_str, 0)
if #args == 0 then if #args == 0 then
args = {1, 1} args = {1, 1}
@ -288,7 +315,7 @@ local function create(data)
width = shares[i], width = shares[i],
height = a.height, height = a.height,
depth = a.depth + 1, depth = a.depth + 1,
group_id = split_count, group_id = op_count,
bl = i == 1 and a.bl or false, bl = i == 1 and a.bl or false,
br = i == #shares and a.br or false, br = i == #shares and a.br or false,
bu = a.bu, bu = a.bu,
@ -305,7 +332,7 @@ local function create(data)
width = a.width, width = a.width,
height = shares[i], height = shares[i],
depth = a.depth + 1, depth = a.depth + 1,
group_id = split_count, group_id = op_count,
bl = a.bl, bl = a.bl,
br = a.br, br = a.br,
bu = i == 1 and a.bu or false, bu = i == 1 and a.bu or false,
@ -315,19 +342,11 @@ local function create(data)
end end
end end
for i = #children, 1, -1 do push_children(children)
if children[i].x ~= math.floor(children[i].x)
or children[i].y ~= math.floor(children[i].y)
or children[i].width ~= math.floor(children[i].width)
or children[i].height ~= math.floor(children[i].height)
then
print("warning, splitting yields floating area " .. _area_tostring(children[i]))
end
open_areas[#open_areas + 1] = children[i]
end
elseif method == "w" then elseif method == "w" then
local a = pop_open_area()
local args = parse_arg_string(arg_str, 0) local args = parse_arg_string(arg_str, 0)
if #args == 0 then if #args == 0 then
args = {1, 1} args = {1, 1}
@ -335,15 +354,13 @@ local function create(data)
args[2] = 1 args[2] = 1
end end
if alt then arg_str = table.reverse(arg_str) end
local h_split, v_split local h_split, v_split
if alt then if alt then
h_split = args[#args] h_split = args[2]
v_split = args[#args - 1] v_split = args[1]
else else
h_split = args[#args - 1] h_split = args[1]
v_split = args[#args] v_split = args[2]
end end
if h_split < 1 then h_split = 1 end if h_split < 1 then h_split = 1 end
if v_split < 1 then v_split = 1 end if v_split < 1 then v_split = 1 end
@ -365,7 +382,7 @@ local function create(data)
width = x_shares[x_index], width = x_shares[x_index],
height = y_shares[y_index], height = y_shares[y_index],
depth = a.depth + 1, depth = a.depth + 1,
group_id = split_count, group_id = op_count,
} }
if x_index == 1 then r.bl = a.bl else r.bl = false end if x_index == 1 then r.bl = a.bl else r.bl = false end
if x_index == h_split then r.br = a.br else r.br = false end if x_index == h_split then r.br = a.br else r.br = false end
@ -375,19 +392,78 @@ local function create(data)
end end
end end
for i = #children, 1, -1 do local merged_children = {}
if children[i].x ~= math.floor(children[i].x) local start_index = 1
or children[i].y ~= math.floor(children[i].y) for i = 3, #args, 2 do
or children[i].width ~= math.floor(children[i].width) -- find the first index that is not merged
or children[i].height ~= math.floor(children[i].height) while start_index <= #children and children[start_index] == false do
then start_index = start_index + 1
print("warning, splitting yields floating area " .. _area_tostring(children[i])) end
if start_index > #children or children[start_index] == false then
break
end
local x = (start_index - 1) % h_split
local y = math.floor((start_index - 1) / h_split)
local w = args[i]
local h = args[i + 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 > h_split then w = h_split - x end
if y + h > v_split then h = v_split - 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 * h_split + tx + 1] == false then
succ = false
break
elseif ty == y then
end_index = ty * h_split + tx + 1
end
end
if not succ then
break
elseif ty > y then
end_index = ty * h_split + x + w
end
end
local r = {
x = children[start_index].x, y = children[start_index].y,
width = children[end_index].x + children[end_index].width - children[start_index].x,
height = children[end_index].y + children[end_index].height - children[start_index].y,
bu = children[start_index].bu, bl = children[start_index].bl,
bd = children[end_index].bd, br = children[end_index].br,
depth = a.depth + 1,
group_id = op_count
}
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 * h_split + tx + 1
if index <= end_index then
children[index] = false
else
break
end
end
end end
open_areas[#open_areas + 1] = children[i]
end end
push_children(merged_children)
push_children(children)
elseif method == "d" then elseif method == "d" then
local a = pop_open_area()
local shares = parse_arg_string(arg_str, 0) local shares = parse_arg_string(arg_str, 0)
local x_shares = {} local x_shares = {}
local y_shares = {} local y_shares = {}
@ -406,9 +482,11 @@ local function create(data)
end end
end end
if #x_shares == 0 or #y_shares == 0 then if #x_shares == 0 then
open_areas[#open_areas + 1] = a open_areas[#open_areas + 1] = a
return return
elseif #y_shares == 0 then
y_shares = {1}
end end
x_shares = fair_split(a.width, x_shares) x_shares = fair_split(a.width, x_shares)
@ -423,7 +501,7 @@ local function create(data)
width = x_shares[x_index], width = x_shares[x_index],
height = y_shares[y_index], height = y_shares[y_index],
depth = a.depth + 1, depth = a.depth + 1,
group_id = split_count, group_id = op_count,
} }
if x_index == 1 then r.bl = a.bl else r.bl = false end if x_index == 1 then r.bl = a.bl else r.bl = false end
if x_index == #x_shares then r.br = a.br else r.br = false end if x_index == #x_shares then r.br = a.br else r.br = false end
@ -433,46 +511,11 @@ local function create(data)
end end
end end
for i = #children, 1, -1 do push_children(children)
if children[i].x ~= math.floor(children[i].x)
or children[i].y ~= math.floor(children[i].y)
or children[i].width ~= math.floor(children[i].width)
or children[i].height ~= math.floor(children[i].height)
then
print("warning, splitting yields floating area " .. _area_tostring(children[i]))
end
open_areas[#open_areas + 1] = children[i]
end
elseif method == "p" then elseif method == "s" then
-- XXX
end
arg_str = ""
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
if arg_str == "" then
push_area()
else
handle_split("w", key == "W")
end
elseif key == "d" or key == "D" then
handle_split("d", key == "D")
elseif key == "p" or key == "P" then
handle_split("p", key == "P")
elseif key == "s" or key == "S" then
if #open_areas > 0 then if #open_areas > 0 then
key = "s"
local times = arg_str == "" and 1 or tonumber(arg_str) local times = arg_str == "" and 1 or tonumber(arg_str)
local t = {} local t = {}
while #open_areas > 0 do while #open_areas > 0 do
@ -481,34 +524,76 @@ local function create(data)
for i = #t, 1, -1 do for i = #t, 1, -1 do
open_areas[#open_areas + 1] = t[(i + times - 1) % #t + 1] open_areas[#open_areas + 1] = t[(i + times - 1) % #t + 1]
end end
arg_str = ""
else
return nil
end end
elseif key == " " or key == "-" then
key = "-" elseif method == "-" then
if arg_str == "" then
push_area() push_area()
else
max_depth = tonumber(arg_str) elseif method == "." then
arg_str = ""
end
elseif key == "Return" or key == "." then
key = "."
while #open_areas > 0 do while #open_areas > 0 do
push_area() push_area()
end end
arg_str = ""
elseif key == "," or tonumber(key) ~= nil then elseif method == ";" then
arg_str = arg_str .. key
else -- nothing
return nil
end end
while #open_areas > 0 and open_areas[#open_areas].depth >= max_depth do while #open_areas > 0 and open_areas[#open_areas].depth >= max_depth do
push_area() push_area()
end end
arg_str = ""
end
local key_translate_tab = {
["Return"] = ".",
[" "] = "-",
}
local ch_info = {
["h"] = 2, ["H"] = 2,
["v"] = 2, ["V"] = 2,
["w"] = 2, ["W"] = 2,
["d"] = 2, ["D"] = 2,
["s"] = 2, ["S"] = 2,
["-"] = 1,
["."] = 1,
[";"] = 1,
["0"] = 0, ["1"] = 0, ["2"] = 0, ["3"] = 0, ["4"] = 0,
["5"] = 0, ["6"] = 0, ["7"] = 0, ["8"] = 0, ["9"] = 0,
[","] = 0,
}
local function handle_ch(key)
if key_translate_tab[key] ~= nil then
key = key_translate_tab[key]
end
local t = ch_info[key]
if t == nil then
return nil
elseif t == 2 then
if pending_op ~= nil then
handle_op(pending_op)
pending_op = key
elseif arg_str == "" then
pending_op = key
else
handle_op(key)
end
elseif t == 1 then
if pending_op ~= nil then
handle_op(pending_op)
pending_op = nil
end
handle_op(key)
elseif t == 0 then
arg_str = arg_str .. key
end
return key return key
end end
@ -575,9 +660,17 @@ local function create(data)
for i, a in ipairs(closed_areas) do for i, a in ipairs(closed_areas) do
local sa = shrink_area_with_gap(a, inner_gap, outer_gap) local sa = shrink_area_with_gap(a, inner_gap, outer_gap)
local to_highlight = false
if pending_op ~= nil then
to_highlight = a.group_id == op_count
end
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
cr:clip() cr:clip()
cr:set_source(closed_color) if to_highlight then
cr:set_source(active_color)
else
cr:set_source(closed_color)
end
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
cr:fill() cr:fill()
cr:set_source(border_color) cr:set_source(border_color)
@ -589,12 +682,18 @@ local function create(data)
for i, a in ipairs(open_areas) do for i, a in ipairs(open_areas) do
local sa = shrink_area_with_gap(a, inner_gap, outer_gap) local sa = shrink_area_with_gap(a, inner_gap, outer_gap)
local to_highlight = false
if pending_op == nil then
to_highlight = i == #open_areas
else
to_highlight = a.group_id == op_count
end
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
cr:clip() cr:clip()
if i == #open_areas then if i == #open_areas then
cr:set_source(api.gears.color(active_color)) cr:set_source(active_color)
else else
cr:set_source(api.gears.color(open_color)) cr:set_source(open_color)
end end
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
cr:fill() cr:fill()
@ -602,12 +701,12 @@ local function create(data)
cr:set_source(border_color) cr:set_source(border_color)
cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height) cr:rectangle(sa.x - start_x, sa.y - start_y, sa.width, sa.height)
cr:set_line_width(10.0) cr:set_line_width(10.0)
if i ~= #open_areas then if to_highlight then
cr:stroke()
else
cr:set_dash({5, 5}, 0) cr:set_dash({5, 5}, 0)
cr:stroke() cr:stroke()
cr:set_dash({}, 0) cr:set_dash({}, 0)
else
cr:stroke()
end end
cr:reset_clip() cr:reset_clip()
end end
@ -653,6 +752,10 @@ local function create(data)
local ok, err = pcall( local ok, err = pcall(
function () function ()
if pending_op ~= nil then
pop_history()
end
if key == "BackSpace" then if key == "BackSpace" then
pop_history() pop_history()
elseif key == "Escape" then elseif key == "Escape" then
@ -675,7 +778,7 @@ local function create(data)
local cmd = data.cmds[cmd_index]:sub(i, i) local cmd = data.cmds[cmd_index]:sub(i, i)
push_history() push_history()
local ret = handle_command(cmd) local ret = handle_ch(cmd)
if ret == nil then if ret == nil then
print("warning: ret is nil") print("warning: ret is nil")
@ -690,7 +793,7 @@ local function create(data)
end end
elseif #open_areas > 0 then elseif #open_areas > 0 then
push_history() push_history()
local ret = handle_command(key) local ret = handle_ch(key)
if ret ~= nil then if ret ~= nil then
current_info = current_info .. ret current_info = current_info .. ret
current_cmd = current_cmd .. ret current_cmd = current_cmd .. ret
@ -745,6 +848,11 @@ local function create(data)
end end
end end
if not to_exit and pending_op ~= nil then
push_history()
handle_op(pending_op)
end
refresh() refresh()
if to_exit then if to_exit then
@ -781,10 +889,9 @@ local function create(data)
local outer_gap = data.outer_gap or data.gap or api.beautiful.useless_gap * 2 or 0 local outer_gap = data.outer_gap or data.gap or api.beautiful.useless_gap * 2 or 0
local inner_gap = data.inner_gap or data.gap or api.beautiful.useless_gap * 2 or 0 local inner_gap = data.inner_gap or data.gap or api.beautiful.useless_gap * 2 or 0
init(init_area) init(init_area)
push_history()
for i = 1, #cmd do for i = 1, #cmd do
local key = handle_command(cmd:sub(i, i)) handle_ch(cmd:sub(i, i))
end end
local areas_with_gap = {} local areas_with_gap = {}

View File

@ -29,7 +29,6 @@ local function with_alpha(col, alpha)
return api.lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha) return api.lgi.cairo.SolidPattern.create_rgba(r, g, b, alpha)
end end
local function start(c) local function start(c)
local tablist_font_desc = api.beautiful.get_merged_font( local tablist_font_desc = api.beautiful.get_merged_font(
api.beautiful.mono_font or api.beautiful.font, api.dpi(10)) api.beautiful.mono_font or api.beautiful.font, api.dpi(10))