From 62699d8ff362e1dbc97fe377efc6c5cd9f2eebee Mon Sep 17 00:00:00 2001 From: Emmanuel Lepage Vallee Date: Mon, 4 Jul 2016 02:43:58 -0400 Subject: [PATCH] widgets: Add a grid layout [WIP] In a quest to get rid of all fit/draw monkeypatching, this commit add a proper grid layout. It is currently as good as the old layout/grid.lua hack, but not "correct" used outside of Radical menus (swap/insert/remove are also broken) and it fail to handle many corner case. --- widgets/grid.lua | 338 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 widgets/grid.lua diff --git a/widgets/grid.lua b/widgets/grid.lua new file mode 100644 index 0000000..933e25a --- /dev/null +++ b/widgets/grid.lua @@ -0,0 +1,338 @@ +--------------------------------------------------------------------------- +-- A two dimension layout disposing the widgets in a grid pattern. +-- +--@DOC_wibox_layout_defaults_grid_EXAMPLE@ +-- @author Emmanuel Lepage Valle +-- @copyright 2016 Emmanuel Lepage Vallee +-- @release @AWESOME_VERSION@ +-- @classmod wibox.layout.grid +--------------------------------------------------------------------------- + +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local base = require("wibox.widget.base") +local table = table +local pairs = pairs +local util = require("awful.util") + +local grid = {} + +local function get_cell_sizes(self, context, orig_width, orig_height) + local width, height = orig_width, orig_height + + local max_col, max_row = {}, {} + + --TODO include spacing + --TODO compute the current sum for each column to have a "less wrong" + -- widget width. + + local max_h = 0 + for row_idx, row in ipairs(self._private.rows) do + for col_idx, v in ipairs(row) do + local w, h = base.fit_widget(self, context, v, width, height) + max_col[col_idx] = math.max(max_col[col_idx] or 0, w) + max_row[row_idx] = math.max(max_col[row_idx] or 0, h) + max_h = math.max(max_h, h) + end + height = height - max_h + end + + return max_col, max_row +end + +-- TODO +-- @param context The context in which we are drawn. +-- @param width The available width. +-- @param height The available height. +function grid:layout(context, width, height) + local max_col, max_row = get_cell_sizes(self, context, width, height) + + local result = {} + local spacing = self._private.spacing --TODO support spacing + + local y = 0 + for row_idx = 1, #max_row do -- Vertical + local x = 0 + for col_idx = 1, #max_col do -- Horizontal + local v = self._private.rows[row_idx][col_idx] + + if v then + table.insert(result, base.place_widget_at(v, x, y, max_col[col_idx], max_row[row_idx])) + x = x + max_col[col_idx] + else + break + end + end + + y = y + max_row[row_idx] + end + + return result +end + +local function get_next_empty(self, row, column) + row, column = row or 1, column or 1 + local cc = self._private.column_count + for i = column, math.huge do + local r = self._private.rows[i] + + if not r then + r = {} + self._private.rows[i] = r + self._private.row_count = i + end + + for j = row, cc do + if not r[j] then + return i, j + end + end + end +end + +local function index_to_point(data, index) + return math.floor(index / data._private.row_count) + 1, + index % data._private.column_count + 1 +end + +--- Add some widgets to the given grid layout +-- @param ... Widgets that should be added (must at least be one) +function grid:add(...) + -- No table.pack in Lua 5.1 :-( + local args = { n=select('#', ...), ... } + assert(args.n > 0, "need at least one widget to add") + + local row, column + + for i=1, args.n do + row, column = get_next_empty(self, row, column) + + base.check_widget(args[i]) + + self._private.rows[row][column] = args[i] + end + + self:emit_signal("widget::layout_changed") +end + +function grid:set_row_count(count) + self._private.row_count = count +end + +function grid:set_column_count(count) + self._private.column_count = count +end + +--- Re-pack all the widget according to the current `row_count` and `column_count`. +function grid:reflow() + + self:emit_signal("widget::layout_changed") +end + +--- Remove a widget from the layout +-- @tparam number index The widget index to remove +-- @treturn boolean index If the operation is successful +function grid:remove(index, reclaim) + local TODO_TOTAL = math.huge --FIXME + if not index or index < 1 or index > TODO_TOTAL then return false end + + local r, c = index_to_point(data, index) + + self._private.rows[r][c] = nil + + self:emit_signal("widget::layout_changed") + + return true +end + +--- Remove one or more widgets from the layout +-- The last parameter can be a boolean, forcing a recursive seach of the +-- widget(s) to remove. +-- @param widget ... Widgets that should be removed (must at least be one) +-- @treturn boolean If the operation is successful +function grid:remove_widgets(...) + local args = { ... } + + local recursive = type(args[#args]) == "boolean" and args[#args] + + local ret = true + for k, rem_widget in ipairs(args) do + if recursive and k == #args then break end + + local idx, l = self:index(rem_widget, recursive) + + if idx and l and l.remove then + l:remove(idx, false) + else + ret = false + end + end + + return #args > (recursive and 1 or 0) and ret +end + +function grid:get_children() + --TODO + return self._private.rows +end + +function grid:set_children(children) + self:reset() + if #children > 0 then + self:add(unpack(children)) + end +end + +--- Replace the first instance of `widget` in the layout with `widget2` +-- @param widget The widget to replace +-- @param widget2 The widget to replace `widget` with +-- @tparam[opt=false] boolean recursive Digg in all compatible layouts to find the widget. +-- @treturn boolean If the operation is successful +function grid:replace_widget(widget, widget2, recursive) + local idx, l = self:index(widget, recursive) + + if idx and l then + l:set(idx, widget2) + return true + end + + return false +end + +function grid:swap_widgets(widget1, widget2, recursive) + base.check_widget(widget1) + base.check_widget(widget2) + + local idx1, l1 = self:index(widget1, recursive) + local idx2, l2 = self:index(widget2, recursive) + + if idx1 and l1 and idx2 and l2 and (l1.set or l1.set_widget) and (l2.set or l2.set_widget) then + if l1.set then + l1:set(idx1, widget2) + elseif l1.set_widget then + l1:set_widget(widget2) + end + if l2.set then + l2:set(idx2, widget1) + elseif l2.set_widget then + l2:set_widget(widget1) + end + + return true + end + + return false +end + +function grid:set(index, widget2) + if (not widget2) or (not self._private.rows[index]) then return false end + + base.check_widget(widget2) + + self._private.rows[index] = widget2 + + self:emit_signal("widget::layout_changed") + + return true +end + +--- Insert a new widget in the layout at position `index` +-- @tparam number index The position +-- @param widget The widget +-- @treturn boolean If the operation is successful +function grid:insert(index, widget) + local TODO_TOTAL = math.huge --FIXME + if not index or index < 1 or index > TODO_TOTAL then return false end + + base.check_widget(widget) + table.insert(self._private.rows, index, widget) --FIXME + self:emit_signal("widget::layout_changed") + + return true +end + +-- Fit the grid layout into the given space +-- @param context The context in which we are fit. +-- @param orig_width The available width. +-- @param orig_height The available height. +function grid:fit(context, orig_width, orig_height) + local max_col, max_row = get_cell_sizes(self, context, orig_width, orig_height) + + -- Now that all fit is done, get the maximum + local used_max_h, used_max_w = 0, 0 + + for row_idx = 1, #max_row do + -- The other widgets will be discarded + if used_max_h + max_row[row_idx] > orig_height then + break + end + + used_max_h = used_max_h + max_row[row_idx] + end + + for col_idx = 1, #max_col do + -- The other widgets will be discarded + if used_max_w + max_col[col_idx] > orig_width then + break + end + + used_max_w = used_max_w + max_col[col_idx] + end + + return used_max_w, used_max_h +end + +function grid:reset() + self._private.rows = {} + self:emit_signal("widget::layout_changed") +end + +--- Set the layout's fill_space property. If this property is true, the last +-- widget will get all the space that is left. If this is false, the last widget +-- won't be handled specially and there can be space left unused. +-- @property fill_space + +function grid:set_fill_space(val) + if self._private.fill_space ~= val then + self._private.fill_space = not not val + self:emit_signal("widget::layout_changed") + end +end + +local function get_layout(dir, widget1, ...) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, grid, true) + + ret._private.widgets = {} + ret:set_spacing(0) + ret:set_fill_space(false) + + if widget1 then + ret:add(widget1, ...) + end + + return ret +end + +--- Add spacing between each layout widgets +-- @property spacing +-- @tparam number spacing Spacing between widgets. + +function grid:set_spacing(spacing) + if self._private.spacing ~= spacing then + self._private.spacing = spacing + self:emit_signal("widget::layout_changed") + end +end + +function grid:get_spacing() + return self._private.spacing or 0 +end + +--@DOC_widget_COMMON@ + +--@DOC_object_COMMON@ + +return setmetatable(grid, {__call=function(_, ...) return get_layout(...) end}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80