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.__ __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? ## Why?
TL;DR --- To bring back the control of the window layout. 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`: the constant name of the layout.
- `name_func`: a `function(t)` closure that returns a string for tag `t`. `name_func` overrides `name`. - `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`. - `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. - `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`). - `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 ### Starting editor in lua
Call `local editor = machi.editor.create()` to create an editor. Call `local editor = machi.editor.create()` to create an editor.
To edit the layout `l` on screen `s`, call `editor.start_interactive(s, l)`. 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(), awful.layout.get(awful.screen.focused()))`. Calling it with no arguments would be the same as `editor.start_interactive(awful.screen.focused())`.
### Basic usage ### 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 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: 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 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. 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 ### Layout command
@ -80,15 +98,24 @@ There are three kinds of operations:
1. Operations taking argument string and parsed as multiple numbers. 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. 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. 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. Otherwise, each digit in the string will be treated as a separated number in type 1 ops.
@ -124,9 +151,9 @@ For examples:
Details: 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: - 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 - `-`: skip the editing of the middle `3` part
- For the right `1` part: - For the right `1` part:
- `12v`: split the right part vertically to the ratio of 1:2 - `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 2 4-4-4
``` ```
`d` command works similarly after the inital grid is defined, such as `d1221012210221212121222`.
### Draft mode ### Draft mode
__This mode is somewhat usable, yet it may change in the future.__ __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. 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 region (ULR) and a bottom-right region (BRR). 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 ULR to the bottom-right corner of the BRR. 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`. To enable draft mode in a layout, configure the layout with a command with a leading `d`, for example, `d12210121`, or `dw66`.
### Nested layouts ### Nested layouts
@ -253,7 +282,7 @@ Known caveats include:
__This feature is not available in draft mode.__ __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. 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`, 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. 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: Calling `machi.switcher.start()` will create a switcher supporting the following keys:
- Arrow keys: move focus into other regions by the direction. - Arrow keys: move focus into other areas 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. - `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) region of the focused window by direction. Only works in draft mode. - `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 regions. - `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. So far, the key binding is not configurable. One has to modify the source code to change it.
## Caveats ## Caveats
1. layout-machi handles `beautiful.useless_gap` slightly differently. A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients.
2. A compositor (e.g. picom, compton, xcompmgr) is required. Otherwise switcher and editor will block the clients.
## License ## 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 layout = require(... .. ".layout")
local editor = require(... .. ".editor") local editor = require(... .. ".editor")
local switcher = require(... .. ".switcher") local switcher = require(... .. ".switcher")
@ -29,9 +30,11 @@ local function get_icon()
end end
return { return {
engine = engine,
layout = layout, layout = layout,
editor = editor, editor = editor,
switcher = switcher, switcher = switcher,
default_name = default_name,
default_editor = default_editor, default_editor = default_editor,
default_layout = default_layout, default_layout = default_layout,
icon_raw = icon_raw, icon_raw = icon_raw,

View File

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

View File

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