From 752539bba5790ecd71a170507dde8efb20811204 Mon Sep 17 00:00:00 2001 From: Xinhao Yuan Date: Wed, 31 Jul 2019 21:00:38 -0400 Subject: [PATCH] Draft mode --- README.md | 51 +++++++++----- editor.lua | 99 +++++++++++++++----------- init.lua | 19 +++-- layout.lua | 191 +++++++++++++++++++++++++++++++++++++-------------- switcher.lua | 143 +++++++++++++++++++++++++------------- 5 files changed, 340 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 8aefe75..8ebfa74 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,12 @@ The editor is keyboard driven, each command is a key with optional digits (namel 1. `Up`/`Down`: restore to the history command sequence 2. `h`/`v`: split the current region horizontally/vertically into `#D` regions. The split will respect the ratio of digits in `D`. 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`. -4. `s`: shift the current editing region with other open regions. If digits are provided, shift for that many times. -5. `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). -6. `Enter`/`.`: close all open regions. When all regions are closed, press `Enter` will save the layout and exit the editor. -7. `Backspace`: undo the last command. -8. `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. +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). +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. +9. `Escape`: exit the editor without saving the layout. For examples: @@ -107,6 +108,32 @@ Tada! ``` +`12210121d` + +``` +11 2222 3333 44 +11 2222 3333 44 + +55 6666 7777 88 +55 6666 7777 88 +55 6666 7777 88 +55 6666 7777 88 + +99 AAAA BBBB CC +99 AAAA BBBB CC +``` + +### Draft mode + +__This mode is experimental. Its usage may change fast.__ + +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. + +This is suppose to work with regions produced with `d` command. +To enable draft mode in a layout, configure the layout with a command with a leading `d`, for example, `d12210121d`. + ### Persistent history By default, the last 100 command sequences are stored in `.cache/awesome/history_machi`. @@ -117,16 +144,12 @@ 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. - - `Tab`: switch windows in the same regions. + - `Shift` + arrow keys: move the focused window to other regions by the direction. In draft mode, move the upper-left region by direction. + - `Control` + arrow keys: move the bottom-right region of the focused window by direction. Only work in draft mode. + - `Tab`: switch beteen windows covering the current regions. So far, the key binding is not configurable. One has to modify the source code to change it. -## Other functions - -`machi.editor.fit_region(c, cycle = false)` will fit a floating client into the closest region. -If `cycle` is true, it then moves the window by cycling all regions. - ## Advanced ### `name` as a function in `machi.layout.create` @@ -142,10 +165,6 @@ To differentiate tags with the same name, you may need a more advanced naming fu 2. True transparency is required. Otherwise switcher and editor will block the clients. -## TODO - - - Tabs on regions? - ## License Apache 2.0 --- See LICENSE diff --git a/editor.lua b/editor.lua index 1c37d66..c32a767 100644 --- a/editor.lua +++ b/editor.lua @@ -62,42 +62,6 @@ local function max(a, b) if a < b then return b else return a end end -local function set_region(c, r) - c.floating = false - c.maximized = false - c.fullscreen = false - c.machi_region = r - api.layout.arrange(c.screen) -end - ---- fit the client into the machi of the screen --- @param c the client to fit --- @param cycle whether to cycle the region if the window is already in machi --- @return whether any actions have been taken on the client -local function fit_region(c, cycle) - local layout = api.layout.get(c.screen) - local regions = layout.machi_get_regions and layout.machi_get_regions(c.screen.workarea, c.screen.selected_tag) - if type(regions) ~= "table" or #regions < 1 then - return false - end - local current_region = c.machi_region or 1 - if not is_tiling(c) then - -- find out which region has the most intersection, calculated by a cap b / a cup b - c.machi_region = machi.layout.find_region(c, regions) - set_tiling(c) - elseif cycle then - if current_region >= #regions then - c.machi_region = 1 - else - c.machi_region = current_region + 1 - end - api.layout.arrange(c.screen) - else - return false - end - return true -end - local function _area_tostring(wa) return "{x:" .. tostring(wa.x) .. ",y:" .. tostring(wa.y) .. ",w:" .. tostring(wa.width) .. ",h:" .. tostring(wa.height) .. "}" end @@ -385,6 +349,64 @@ local function create(data) open_areas[#open_areas + 1] = children[i] end + elseif method == "d" then + + local x_shares = {} + local y_shares = {} + + local current = x_shares + for i = 1, #args do + local arg + if not alt then + arg = tonumber(args:sub(i, i)) + else + arg = tonumber(args:sub(#args - i + 1, #args - i + 1)) + end + if arg == 0 then + if current == x_shares then current = y_shares else break end + else + current[#current + 1] = arg + end + end + + if #x_shares == 0 or #y_shares == 0 then + open_areas[#open_areas + 1] = a + return + end + + x_shares = fair_split(a.width, x_shares) + y_shares = fair_split(a.height, y_shares) + + local children = {} + for y_index = 1, #y_shares do + for x_index = 1, #x_shares do + local r = { + x = x_index == 1 and a.x or children[#children].x + children[#children].width, + y = y_index == 1 and a.y or (x_index == 1 and children[#children].y + children[#children].height or children[#children].y), + width = x_shares[x_index], + height = y_shares[y_index], + depth = a.depth + 1, + group_id = split_count, + } + 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 y_index == 1 then r.bu = a.bu else r.bu = false end + if y_index == #y_shares then r.bd = a.bd else r.bd = false end + children[#children + 1] = r + end + end + + for i = #children, 1, -1 do + 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 -- XXX end @@ -407,6 +429,8 @@ local function create(data) 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 @@ -578,7 +602,6 @@ local function create(data) infobox.bgimage = draw_info end - print("interactive layout editing starts") init(screen.workarea) @@ -760,8 +783,6 @@ end return { - set_region = set_region, - fit_region = fit_region, create = create, restore_data = restore_data, } diff --git a/init.lua b/init.lua index cdc8047..69f5463 100644 --- a/init.lua +++ b/init.lua @@ -1,17 +1,16 @@ local layout = require(... .. ".layout") local editor = require(... .. ".editor") local switcher = require(... .. ".switcher") +local function default_name(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, true +end local default_editor = editor.create() -local default_layout = layout.create( - 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, true - end, - default_editor) +local default_layout = layout.create(default_name, default_editor) local gcolor = require("gears.color") local beautiful = require("beautiful") diff --git a/layout.lua b/layout.lua index 16443cb..30ca4ff 100644 --- a/layout.lua +++ b/layout.lua @@ -44,6 +44,42 @@ local function find_region(c, regions) return choice end +local function distance(x1, y1, x2, y2) + -- use d1 + return math.abs(x1 - x2) + math.abs(y1 - y2) +end + +local function find_lu(c, regions) + local lu = nil + for i, a in ipairs(regions) do + 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 + return lu +end + +local function find_rd(c, regions, lu) + assert(lu ~= nil) + local rd = nil + for i, a in ipairs(regions) do + if a.x + a.width > regions[lu].x and a.y + a.height > regions[lu].y then + if rd == nil or distance(c.x + c.width, c.y + c.height, a.x + a.width, a.y + a.height) < distance(c.x + c.width, c.y + c.height, regions[rd].x + regions[rd].width, regions[rd].y + regions[rd].height) then + rd = i + end + end + end + return rd +end + +local function set_geometry(c, region_lu, region_rd, useless_gap, border_width) + -- We try to negate the gap of outer layer8 + c.x = region_lu.x - useless_gap + c.y = region_lu.y - useless_gap + c.width = region_rd.x + region_rd.width - region_lu.x + (useless_gap - border_width) * 2 + c.height = region_rd.y + region_rd.height - region_lu.y + (useless_gap - border_width) * 2 +end + local function create(name, editor) local instances = {} @@ -67,13 +103,13 @@ local function create(name, editor) local function get_regions(workarea, tag) local instance = get_instance(tag) - if instance.cmd == nil then return {} end + if instance.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] = editor.run_cmd(workarea, instance.cmd) end - return instance.regions_cache[key] + return instance.regions_cache[key], instance.cmd:sub(1,1) == "d" end local function set_cmd(cmd, tag) @@ -88,69 +124,122 @@ local function create(name, editor) 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 regions = get_regions(wa, get_screen(p.screen).selected_tag) + local regions, draft_mode = get_regions(wa, get_screen(p.screen).selected_tag) if #regions == 0 then return end - for i, c in ipairs(cls) do - if c.floating then - print("Ignore client " .. tostring(c)) - else - if c.machi_region == nil then - c.machi_region = find_region(c, regions) - elseif c.machi_region > #regions then - c.machi_region = #regions - elseif c.machi_region <= 1 then - c.machi_region = 1 + if draft_mode then + for i, c in ipairs(cls) do + if c.floating then + 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 + print("Compute regions for " .. c.name) + lu = find_lu(c, regions) + if lu ~= nil then + rd = find_rd(c, regions, lu) + end + end + + if lu ~= nil and rd ~= nil then + c.machi_lu, c.machi_rd = lu, rd + p.geometries[c] = {} + set_geometry(p.geometries[c], regions[lu], regions[rd], useless_gap, 0) + end + end + end + else + for i, c in ipairs(cls) do + if c.floating then + print("Ignore client " .. tostring(c)) + else + if c.machi_region ~= 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 + print("Compute regions for " .. c.name) + local region = find_region(c, regions) + c.machi_region = region + p.geometries[c] = {} + set_geometry(p.geometries[c], regions[region], regions[region], useless_gap, 0) + end end - local region = c.machi_region - - -- Editor already handled useless_gap in the stored regions. - -- We try to negate the gap of outer layer. - p.geometries[c] = { - 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, - } - - print("Put client " .. tostring(c) .. " to region " .. region) - end end end - -- move the closest region regardingly to the center distance - local function resize_handler(c, context, h) - if context ~= "mouse.move" then return end - + local function resize_handler (c, context, h) local workarea = c.screen.workarea - local regions = get_regions(workarea, c.screen.selected_tag) + local regions, draft_mode = get_regions(workarea, c.screen.selected_tag) if #regions == 0 then return end - local center_x = h.x + h.width / 2 - local center_y = h.y + h.height / 2 + if draft_mode then + local lu = find_lu(h, regions) + local rd = nil + if lu ~= nil then + if context == "mouse.move" then + local hh = {} + hh.x = regions[lu].x + hh.y = regions[lu].y + hh.width = h.width + hh.height = h.height + rd = find_rd(hh, regions, lu) + else + rd = find_rd(h, regions, lu) + end - local choice = 1 - local choice_value = nil - - 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 then + c.machi_lu = lu + c.machi_rd = rd + set_geometry(c, regions[lu], regions[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 - c.x = regions[choice].x - c.y = regions[choice].y - c.width = max(1, regions[choice].width - 2 * c.border_width) - c.height = max(1, regions[choice].height - 2 * c.border_width) + local workarea = c.screen.workarea + local regions = get_regions(workarea, c.screen.selected_tag) + + if #regions == 0 then return end + + 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(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 + end + end + + if c.machi_region ~= choice then + c.machi_region = choice + set_geometry(c, regions[choice], regions[choice], 0, c.border_width) + end end end @@ -166,5 +255,5 @@ end return { create = create, - find_region = find_region, + set_geometry = set_geometry, } diff --git a/switcher.lua b/switcher.lua index e37abc2..a63e52a 100644 --- a/switcher.lua +++ b/switcher.lua @@ -1,3 +1,7 @@ +local machi = { + layout = require((...):match("(.-)[^%.]+$") .. "layout"), +} + local api = { client = client, beautiful = require("beautiful"), @@ -11,19 +15,6 @@ local api = { dpi = require("beautiful.xresources").apply_dpi, } --- -- Seems not needed? --- local focus_timer = 0 --- api.client.connect_signal( --- "focus", --- function (c) --- if c.focus_timer == nil or c.focus_timer < focus_timer then --- c.focus_timer = focus_timer --- end --- focus_timer = c.focus_timer + 1 --- c.focus_timer = focus_timer --- end --- ) - local function min(a, b) if a < b then return a else return b end end @@ -59,7 +50,7 @@ local function start(c) local layout = api.layout.get(screen) if c.floating or layout.machi_get_regions == nil then return end - local regions = layout.machi_get_regions(c.screen.workarea, c.screen.selected_tag) + local regions, draft_mode = layout.machi_get_regions(c.screen.workarea, c.screen.selected_tag) local infobox = api.wibox({ screen = screen, @@ -73,6 +64,7 @@ local function start(c) }) infobox.visible = true + local tablist_region = nil local tablist = nil local tablist_index = nil @@ -83,12 +75,13 @@ local function start(c) if tablist == nil then tablist = {} for _, tc in ipairs(screen.tiled_clients) do - if tc.machi_region == c.machi_region - and not tc.maximized - and not tc.maximized_horizontal - and not tc.maximized_vertical + if not (tc.floating or tc.maximized or tc.maximized_horizontal or tc.maximized_vertical) then - tablist[#tablist + 1] = tc + if tc.x <= traverse_x and traverse_x < tc.x + tc.width and + tc.y <= traverse_y and traverse_y < tc.y + tc.height + then + tablist[#tablist + 1] = tc + end end end @@ -108,7 +101,8 @@ local function start(c) cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) cr:clip() - if i == c.machi_region then + 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 local pl = api.lgi.Pango.Layout.create(cr) pl:set_font_description(tablist_font_desc) @@ -132,7 +126,9 @@ local function start(c) local x_offset = a.x + a.width / 2 - start_x 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 + cr:rectangle(a.x - start_x, a.y - start_y, a.width, a.height) cr:set_source(fill_color) cr:fill() @@ -192,10 +188,33 @@ local function start(c) infobox.bgimage = draw_info end elseif key == "Up" or key == "Down" or key == "Left" or key == "Right" then + local shift = false + local ctrl = false + for i, m in ipairs(mod) do + if m == "Shift" then shift = true + elseif m == "Control" then ctrl = true + end + end + + if shift then + traverse_x = c.x + traverse_radius + traverse_y = c.y + traverse_radius + elseif ctrl then + traverse_x = c.x + c.width - c.border_width * 2 - traverse_radius + traverse_y = c.y + c.height - c.border_width * 2 - traverse_radius + end + local choice = nil local choice_value + local current_region = nil 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 + local v if key == "Up" then if a.x < traverse_x + threshold @@ -233,43 +252,73 @@ local function start(c) end end - if choice ~= nil and choice_value > threshold then - local shift = false - for i, m in ipairs(mod) do - if m == "Shift" then shift = true end + if choice == nil then + choice = current_region + if key == "Up" then + traverse_y = screen.workarea.y + elseif key == "Down" then + traverse_y = screen.workarea.y + screen.workarea.height + elseif key == "Left" then + traverse_x = screen.workarea.x + else + traverse_x = screen.workarea.x + screen.workarea.width end + end - local move_traverse = false - + if choice ~= nil then if shift then - -- move the window - c.machi_region = choice + if draft_mode then + -- move the left-up region + local lu = choice + local rd = c.machi_rd + if regions[rd].x + regions[rd].width <= regions[lu].x or + regions[rd].y + regions[rd].height <= regions[lu].y + then + rd = lu + end + machi.layout.set_geometry(c, regions[lu], regions[rd], 0, c.border_width) + c.machi_lu = lu + c.machi_rd = rd + else + -- move the window + machi.layout.set_geometry(c, regions[choice], regions[choice], 0, c.border_width) + c.machi_region = choice + end c:emit_signal("request::activate", "mouse.move", {raise=false}) c:raise() api.layout.arrange(screen) - move_traverse = true + + tablist = nil + elseif ctrl and draft_mode then + -- move the right-down region + local lu = c.machi_lu + local rd = choice + if regions[rd].x + regions[rd].width <= regions[lu].x or + regions[rd].y + regions[rd].height <= regions[lu].y + then + lu = rd + end + machi.layout.set_geometry(c, regions[lu], regions[rd], 0, c.border_width) + c.machi_lu = lu + c.machi_rd = rd + + c:emit_signal("request::activate", "mouse.move", {raise=false}) + c:raise() + api.layout.arrange(screen) + + tablist = nil else -- move the focus - for _, tc in ipairs(screen.tiled_clients) do - if tc.machi_region == choice - and not tc.maximized - and not tc.maximized_horizontal - and not tc.maximized_vertical - then - c = tc - api.client.focus = c - break - end - end - move_traverse = true - end - - if move_traverse 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)) tablist = nil + ensure_tablist() + if #tablist > 0 and tablist[1] ~= c then + c = tablist[1] + api.client.focus = c + end end + 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)) infobox.bgimage = draw_info end elseif key == "Escape" or key == "Return" then