diff --git a/awesome/src/dbus/bluetooth_dbus.lua b/awesome/src/dbus/bluetooth_dbus.lua index 8511ebd..8c51a0a 100644 --- a/awesome/src/dbus/bluetooth_dbus.lua +++ b/awesome/src/dbus/bluetooth_dbus.lua @@ -1,3 +1,4 @@ +local awful = require("awful") local gears = require("gears") local dbus_proxy = require("dbus_proxy") local lgi = require("lgi") @@ -41,83 +42,91 @@ return function() end end - local ObjectManager = dbus_proxy.Proxy:new { - bus = dbus_proxy.Bus.SYSTEM, - name = "org.bluez", - interface = "org.freedesktop.DBus.ObjectManager", - path = "/" - } + awful.spawn.easy_async_with_shell( + "lsusb | grep Bluetooth", + function(stdout) + stdout = stdout:gsub("\n", "") + if stdout ~= "" or stdout == nil then + local ObjectManager = dbus_proxy.Proxy:new { + bus = dbus_proxy.Bus.SYSTEM, + name = "org.bluez", + interface = "org.freedesktop.DBus.ObjectManager", + path = "/" + } - local Adapter = dbus_proxy.Proxy:new { - bus = dbus_proxy.Bus.SYSTEM, - name = "org.bluez", - interface = "org.bluez.Adapter1", - path = "/org/bluez/hci0" - } + local Adapter = dbus_proxy.Proxy:new { + bus = dbus_proxy.Bus.SYSTEM, + name = "org.bluez", + interface = "org.bluez.Adapter1", + path = "/org/bluez/hci0" + } - local AdapterProperties = dbus_proxy.Proxy:new { - bus = dbus_proxy.Bus.SYSTEM, - name = "org.bluez", - interface = "org.freedesktop.DBus.Properties", - path = "/org/bluez/hci0" - } + local AdapterProperties = dbus_proxy.Proxy:new { + bus = dbus_proxy.Bus.SYSTEM, + name = "org.bluez", + interface = "org.freedesktop.DBus.Properties", + path = "/org/bluez/hci0" + } - ObjectManager:connect_signal( - function(interface) - get_device_info(interface) - end, - "InterfacesAdded" - ) + ObjectManager:connect_signal( + function(interface) + get_device_info(interface) + end, + "InterfacesAdded" + ) - ObjectManager:connect_signal( - function(interface) - awesome.emit_signal("device_removed", interface) - end, - "InterfacesRemoved" - ) + ObjectManager:connect_signal( + function(interface) + awesome.emit_signal("device_removed", interface) + end, + "InterfacesRemoved" + ) - Adapter:connect_signal( - function(data) - if data.Powered ~= nil then - awesome.emit_signal("state", data.Powered) + Adapter:connect_signal( + function(data) + if data.Powered ~= nil then + awesome.emit_signal("state", data.Powered) + end + end, + "PropertiesChanged" + ) + + AdapterProperties:connect_signal( + function(data) + if data.Powered ~= nil then + awesome.emit_signal("state", data.Powered) + if data.Powered then + Adapter:StartDiscovery() + end + end + end, + "PropertiesChanged" + ) + + awesome.connect_signal("toggle_bluetooth", + function() + local is_powered = Adapter.Powered + Adapter:Set( + "org.bluez.Adapter1", + "Powered", + lgi.GLib.Variant("b", not is_powered) + ) + Adapter.Powered = { signature = "b", value = not is_powered } + awesome.emit_signal("state", Adapter.Powered) + end) + + gears.timer.delayed_call( + function() + local objects = ObjectManager:GetManagedObjects() + + for object_path, _ in pairs(objects) do + get_device_info(object_path) + end + + awesome.emit_signal("state", Adapter.Powered) + end + ) end - end, - "PropertiesChanged" - ) - - AdapterProperties:connect_signal( - function(data) - if data.Powered ~= nil then - awesome.emit_signal("state", data.Powered) - if data.Powered then - Adapter:StartDiscovery() - end - end - end, - "PropertiesChanged" - ) - - awesome.connect_signal("toggle_bluetooth", - function() - local is_powered = Adapter.Powered - Adapter:Set( - "org.bluez.Adapter1", - "Powered", - lgi.GLib.Variant("b", not is_powered) - ) - Adapter.Powered = { signature = "b", value = not is_powered } - awesome.emit_signal("state", Adapter.Powered) - end) - - gears.timer.delayed_call( - function() - local objects = ObjectManager:GetManagedObjects() - - for object_path, _ in pairs(objects) do - get_device_info(object_path) - end - - awesome.emit_signal("state", Adapter.Powered) end ) diff --git a/awesome/src/lib/overflow_widget/overflow.lua b/awesome/src/lib/overflow_widget/overflow.lua index a4511bb..58c3acb 100644 --- a/awesome/src/lib/overflow_widget/overflow.lua +++ b/awesome/src/lib/overflow_widget/overflow.lua @@ -7,6 +7,7 @@ -- @author Lucas Schwiderski -- @copyright 2021 Lucas Schwiderski -- @layoutmod wibox.layout.overflow +-- @supermodule wibox.layout.fixed --------------------------------------------------------------------------- local base = require('wibox.widget.base') @@ -46,32 +47,26 @@ function overflow:fit(context, orig_width, orig_height) -- First, determine widget sizes. -- Only when the content doesn't fit and needs scrolling should -- we reduce content size to make space for a scrollbar. - for _, widget in pairs(widgets) do + for _, widget in ipairs(widgets) do local w, h = base.fit_widget(self, context, widget, width, height) if is_y then used_max = math.max(used_max, w) used_in_dir = used_in_dir + h else - used_in_dir = used_in_dir + w used_max = math.max(used_max, h) + used_in_dir = used_in_dir + w end end local spacing = self._private.spacing * (num_widgets - 1) used_in_dir = used_in_dir + spacing - local need_scrollbar = used_in_dir > avail_in_dir and scrollbar_enabled + local need_scrollbar = scrollbar_enabled and used_in_dir > avail_in_dir - -- If the direction perpendicular to scrolling (e.g. width in vertical - -- scrolling) is not fully covered by any of the widgets, we can add our - -- scrollbar width to that value. Otherwise widget size will be reduced - -- during `layout` to make space for the scrollbar. - if need_scrollbar - and ( - (is_y and used_max < orig_width) - or (not is_y and used_max < orig_height) - ) then + -- Even if `used_max == orig_(width|height)` already, `base.fit_widget` + -- will clamp return values, so we can "overextend" here. + if need_scrollbar then used_max = used_max + scrollbar_width end @@ -114,8 +109,8 @@ function overflow:layout(context, orig_width, orig_height) used_max = math.max(used_max, w) used_in_dir = used_in_dir + h else - used_in_dir = used_in_dir + w used_max = math.max(used_max, h) + used_in_dir = used_in_dir + w end end @@ -127,7 +122,7 @@ function overflow:layout(context, orig_width, orig_height) local need_scrollbar = used_in_dir > avail_in_dir and scrollbar_enabled - local scroll_position = self._private.position + local scroll_position = self._private.scroll_factor if need_scrollbar then local scrollbar_widget = self._private.scrollbar_widget @@ -139,7 +134,7 @@ function overflow:layout(context, orig_width, orig_height) -- Make scrollbar length reflect `visible_percent` -- TODO: Apply a default minimum length local bar_length = math.floor(visible_percent * avail_in_dir) - local bar_pos = (avail_in_dir - bar_length) * self._private.position + local bar_pos = (avail_in_dir - bar_length) * self._private.scroll_factor if is_y then bar_w, bar_h = base.fit_widget(self, context, scrollbar_widget, scrollbar_width, bar_length) @@ -191,7 +186,7 @@ function overflow:layout(context, orig_width, orig_height) end end - for i, w in pairs(widgets) do + for i, w in ipairs(widgets) do local content_x, content_y local content_w, content_h = base.fit_widget(self, context, w, width, height) @@ -263,19 +258,13 @@ function overflow:before_draw_children(_, cr, width, height) end --- The amount of units to advance per scroll event. +-- -- This affects calls to `scroll` and the default mouse wheel handler. -- -- The default is `10`. -- -- @property step -- @tparam number step The step size. --- @see set_step - ---- Set the step size. --- --- @method overflow:set_step --- @tparam number step The step size. --- @see step function overflow:set_step(step) self._private.step = step -- We don't need to emit enything here, since changing step only really @@ -283,15 +272,18 @@ function overflow:set_step(step) end --- Scroll the layout's content by `amount * step`. +-- -- A positive amount scroll down/right, a negative amount scrolls up/left. -- +-- The amount of units scrolled is affected by `step`. +-- -- @method overflow:scroll -- @tparam number amount The amount to scroll by. --- @emits property::overflow::position --- @emitstparam property::overflow::position number position The new position. +-- @emits property::overflow::scroll_factor +-- @emitstparam property::overflow::scroll_factor number scroll_factor The new +-- scroll factor. -- @emits widget::layout_changed -- @emits widget::redraw_needed --- @see step function overflow:scroll(amount) if amount == 0 then return @@ -299,55 +291,45 @@ function overflow:scroll(amount) local interval = self._private.used_in_dir local delta = self._private.step / interval - local pos = self._private.position + (delta * amount) - self:set_position(pos) + local factor = self._private.scroll_factor + (delta * amount) + self:set_scroll_factor(factor) end ---- The scroll position. --- The position is represented as a fraction from `0` to `1`. +--- The scroll factor. -- --- @property position --- @tparam number position The position. +-- The scroll factor represents how far the layout's content is currently +-- scrolled. It is represented as a fraction from `0` to `1`, where `0` is the +-- start of the content and `1` is the end. +-- +-- @property scroll_factor +-- @tparam number scroll_factor The scroll factor. -- @propemits true false --- @see set_position ---- Set the current scroll position. --- --- @method overflow:set_position --- @tparam number position The new position. --- @propemits true false --- @emits widget::layout_changed --- @emits widget::redraw_needed --- @see position -function overflow:set_position(pos) - local current = self._private.position +function overflow:set_scroll_factor(factor) + local current = self._private.scroll_factor local interval = self._private.used_in_dir - self._private.avail_in_dir - if current == pos + if current == factor -- the content takes less space than what is available, i.e. everything -- is already visible or interval <= 0 - -- the position is out of range - or (current <= 0 and pos < 0) - or (current >= 1 and pos > 1) then + -- the scroll factor is out of range + or (current <= 0 and factor < 0) + or (current >= 1 and factor > 1) then return end - self._private.position = math.min(1, math.max(pos, 0)) + self._private.scroll_factor = math.min(1, math.max(factor, 0)) self:emit_signal("widget::layout_changed") - self:emit_signal("property::position", pos) + self:emit_signal("property::scroll_factor", factor) end ---- Get the current scroll position. --- --- @method overflow:get_position --- @treturn number position The current position. --- @see position -function overflow:get_position() - return self._private.position +function overflow:get_scroll_factor() + return self._private.scroll_factor end --- The scrollbar width. +-- -- For horizontal scrollbars, this is the scrollbar height -- -- The default is `5`. @@ -357,16 +339,7 @@ end -- @property scrollbar_width -- @tparam number scrollbar_width The scrollbar width. -- @propemits true false --- @see set_scrollbar_width ---- Set the scrollbar width. --- --- @method overflow:set_scrollbar_width --- @tparam number scrollbar_width The new scrollbar width. --- @propemits true false --- @emits widget::layout_changed --- @emits widget::redraw_needed --- @see scrollbar_width function overflow:set_scrollbar_width(width) if self._private.scrollbar_width == width then return @@ -382,23 +355,14 @@ end -- -- For horizontal scrollbars, this can be `"top"` or `"bottom"`, -- for vertical scrollbars this can be `"left"` or `"right"`. --- The default is `"left"`/`"bottom"`. +-- The default is `"right"`/`"bottom"`. -- --@DOC_wibox_layout_overflow_scrollbar_position_EXAMPLE@ -- -- @property scrollbar_position -- @tparam string scrollbar_position The scrollbar position. -- @propemits true false --- @see set_scrollbar_position ---- Set the scrollbar position. --- --- @method overflow:set_scrollbar_position --- @tparam string scrollbar_position The new scrollbar position. --- @propemits true false --- @emits widget::layout_changed --- @emits widget::redraw_needed --- @see scrollbar_position function overflow:set_scrollbar_position(position) if self._private.scrollbar_position == position then return @@ -410,7 +374,12 @@ function overflow:set_scrollbar_position(position) self:emit_signal("property::scrollbar_position", position) end +function overflow:get_scrollbar_position() + return self._private.scrollbar_position +end + --- The scrollbar visibility. +-- -- If this is set to `false`, no scrollbar will be rendered, even if the layout's -- content overflows. Mouse wheel scrolling will work regardless. -- @@ -419,16 +388,7 @@ end -- @property scrollbar_enabled -- @tparam boolean scrollbar_enabled The scrollbar visibility. -- @propemits true false --- @see set_scrollbar_enabled ---- Enable or disable the scrollbar visibility. --- --- @method overflow:set_scrollbar_enabled --- @tparam boolean scrollbar_enabled The new scrollbar visibility. --- @propemits true false --- @emits widget::layout_changed --- @emits widget::redraw_needed --- @see scrollbar_enabled function overflow:set_scrollbar_enabled(enabled) if self._private.scrollbar_enabled == enabled then return @@ -440,22 +400,34 @@ function overflow:set_scrollbar_enabled(enabled) self:emit_signal("property::scrollbar_enabled", enabled) end +function overflow:get_scrollbar_enabled() + return self._private.scrollbar_enabled +end + -- Wraps a callback function for `mousegrabber` that is capable of --- updating the scroll position. -local function build_grabber(container) +-- updating the scroll factor. +local function build_grabber(container, initial_x, initial_y, geo) local is_y = container._private.dir == "y" local bar_interval = container._private.avail_in_dir - container._private.bar_length - local start_pos = container._private.position * bar_interval - local coords = mouse.coords() - local start = is_y and coords.y or coords.x + local start_pos = container._private.scroll_factor * bar_interval + local start = is_y and initial_y or initial_x + + -- Calculate a matrix transforming from screen coordinates into widget + -- coordinates. + -- This is required for mouse movement to work when the widget has been + -- transformed by something like `wibox.container.rotate`. + local matrix_from_device = geo.hierarchy:get_matrix_from_device() + local wgeo = geo.drawable.drawable:geometry() + local matrix = matrix_from_device:translate(-wgeo.x, -wgeo.y) return function(mouse) if not mouse.buttons[1] then return false end - local pos = is_y and mouse.y or mouse.x - container:set_position((start_pos + (pos - start)) / bar_interval) + local x, y = matrix:transform_point(mouse.x, mouse.y) + local pos = is_y and x and y + container:set_scroll_factor((start_pos + (pos - start)) / bar_interval) return true end @@ -463,35 +435,25 @@ end -- Applies a mouse button signal using `build_grabber` to a scrollbar widget. local function apply_scrollbar_mouse_signal(container, w) - w:connect_signal('button::press', function(_, _, _, button_id) + w:connect_signal('button::press', function(_, x, y, button_id, _, geo) if button_id ~= 1 then return end - mousegrabber.run(build_grabber(container), "fleur") + mousegrabber.run(build_grabber(container, x, y, geo), "fleur") end) end --- The scrollbar widget. -- This widget is rendered as the scrollbar element. -- --- The default is `awful.widget.separator{ shape = gears.shape.rectangle }`. +-- The default is `wibox.widget.separator{ shape = gears.shape.rectangle }`. -- --@DOC_wibox_layout_overflow_scrollbar_widget_EXAMPLE@ -- -- @property scrollbar_widget -- @tparam widget scrollbar_widget The scrollbar widget. -- @propemits true false --- @see set_scrollbar_widget ---- Set the scrollbar widget. --- --- This will also apply the mouse button handler. --- --- @method overflow:set_scrollbar_widget --- @tparam widget scrollbar_widget The new scrollbar widget. --- @propemits true false --- @emits widget::layout_changed --- @see scrollbar_widget function overflow:set_scrollbar_widget(widget) local w = base.make_widget_from_value(widget) @@ -503,14 +465,22 @@ function overflow:set_scrollbar_widget(widget) self:emit_signal("property::scrollbar_widget", widget) end +function overflow:get_scrollbar_widget() + return self._private.scrollbar_widget +end + local function new(dir, ...) local ret = fixed[dir](...) gtable.crush(ret, overflow, true) ret.widget_name = gobject.modulename(2) + -- Tell the widget system to prevent clicks outside the layout's extends + -- to register with child widgets, even if they actually extend that far. + -- This prevents triggering button presses on hidden/clipped widgets. + ret.clip_child_extends = true - -- Manually set the position here. We don't know the bounding size yet. - ret._private.position = 0 + -- Manually set the scroll factor here. We don't know the bounding size yet. + ret._private.scroll_factor = 0 -- Apply defaults. Bypass setters to avoid signals. ret._private.step = 10 @@ -525,17 +495,9 @@ local function new(dir, ...) ret:connect_signal('button::press', function(self, _, _, button) if button == 4 then - if self.scroll_speed == nil or self.scroll_speed <= 0 then - self:scroll(-1) - else - self:scroll(-1 * self.scroll_speed) - end + self:scroll(-1) elseif button == 5 then - if self.scroll_speed == nil or self.scroll_speed <= 0 then - self:scroll(1) - else - self:scroll(1 * self.scroll_speed) - end + self:scroll(1) end end) @@ -559,58 +521,9 @@ end -- widgets exceeds the height available whithin the layout's outer container -- a scrollbar will be added and scrolling behavior enabled. -- @tparam widget ... Widgets that should be added to the layout. --- @constructorfct wibox.layout.fixed.horizontal +-- @constructorfct wibox.layout.overflow.horizontal function overflow.vertical(...) return new("vertical", ...) end ---- Add spacing between each layout widgets. --- --- This behaves just like in `wibox.layout.fixed`: --- ---@DOC_wibox_layout_fixed_spacing_EXAMPLE@ --- --- @property spacing --- @tparam number spacing Spacing between widgets. --- @propemits true false --- @see wibox.layout.fixed - - ---- The widget used to fill the spacing between the layout elements. --- By default, no widget is used. --- --- This behaves just like in `wibox.layout.fixed`: --- ---@DOC_wibox_layout_fixed_spacing_widget_EXAMPLE@ --- --- @property spacing_widget --- @tparam widget spacing_widget --- @propemits true false --- @see wibox.layout.fixed - - ---- Set the layout's fill_space property. --- --- If this property is `true`, widgets --- take all space in the non-scrolling directing (e.g. `width` for vertical --- scrolling). If `false`, they will only take as much as they need for their --- content. --- --- The default is `true`. --- ---@DOC_wibox_layout_overflow_fill_space_EXAMPLE@ --- --- @property fill_space --- @tparam boolean fill_space --- @propemits true false - - ---@DOC_fixed_COMMON@ - ---@DOC_widget_COMMON@ - ---@DOC_object_COMMON@ - return setmetatable(overflow, overflow.mt) - --- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/awesome/wibox/hierarchy.lua b/awesome/wibox/hierarchy.lua new file mode 100644 index 0000000..ea8b179 --- /dev/null +++ b/awesome/wibox/hierarchy.lua @@ -0,0 +1,381 @@ +--------------------------------------------------------------------------- +-- Management of widget hierarchies. Each widget hierarchy object has a widget +-- for which it saves e.g. size and transformation in its parent. Also, each +-- widget has a number of children. +-- +-- @author Uli Schlachter +-- @copyright 2015 Uli Schlachter +-- @classmod wibox.hierarchy +--------------------------------------------------------------------------- + +local matrix = require("gears.matrix") +local protected_call = require("gears.protected_call") +local cairo = require("lgi").cairo +local base = require("wibox.widget.base") +local no_parent = base.no_parent_I_know_what_I_am_doing + +local hierarchy = {} +local widgets_to_count = setmetatable({}, { __mode = "k" }) + +--- Add a widget to the list of widgets for which hierarchies should count their +-- occurrences. Note that for correct operations, the widget must not yet be +-- visible in any hierarchy. +-- @param widget The widget that should be counted. +-- @staticfct wibox.hierarchy.count_widget +function hierarchy.count_widget(widget) + widgets_to_count[widget] = true +end + +local function hierarchy_new(redraw_callback, layout_callback, callback_arg) + local result = { + _matrix = matrix.identity, + _matrix_to_device = matrix.identity, + _need_update = true, + _widget = nil, + _context = nil, + _redraw_callback = redraw_callback, + _layout_callback = layout_callback, + _callback_arg = callback_arg, + _size = { + width = nil, + height = nil + }, + _draw_extents = { + x = 0, + y = 0, + width = 0, + height = 0 + }, + _parent = nil, + _children = {}, + _widget_counts = {}, + } + + function result._redraw() + redraw_callback(result, callback_arg) + end + + function result._layout() + local h = result + while h do + h._need_update = true + h = h._parent + end + layout_callback(result, callback_arg) + end + + function result._emit_recursive(widget, name, ...) + local cur = result + assert(widget == cur._widget) + while cur do + if cur._widget then + cur._widget:emit_signal(name, ...) + end + cur = cur._parent + end + end + + for k, f in pairs(hierarchy) do + if type(f) == "function" then + result[k] = f + end + end + return result +end + +local hierarchy_update +function hierarchy_update(self, context, widget, width, height, region, matrix_to_parent, matrix_to_device) + if (not self._need_update) and self._widget == widget and + self._context == context and + self._size.width == width and self._size.height == height and + matrix.equals(self._matrix, matrix_to_parent) and + matrix.equals(self._matrix_to_device, matrix_to_device) then + -- Nothing changed + return + end + + self._need_update = false + + local old_x, old_y, old_width, old_height + local old_widget = self._widget + if self._size.width and self._size.height then + local x, y, w, h = matrix.transform_rectangle(self._matrix_to_device, 0, 0, self._size.width, self._size.height) + old_x, old_y = math.floor(x), math.floor(y) + old_width, old_height = math.ceil(x + w) - old_x, math.ceil(y + h) - old_y + else + old_x, old_y, old_width, old_height = 0, 0, 0, 0 + end + + -- Disconnect old signals + if old_widget and old_widget ~= widget then + self._widget:disconnect_signal("widget::redraw_needed", self._redraw) + self._widget:disconnect_signal("widget::layout_changed", self._layout) + self._widget:disconnect_signal("widget::emit_recursive", self._emit_recursive) + end + + -- Save the arguments we need to save + self._widget = widget + self._context = context + self._size.width = width + self._size.height = height + self._matrix = matrix_to_parent + self._matrix_to_device = matrix_to_device + + -- Connect signals + if old_widget ~= widget then + widget:weak_connect_signal("widget::redraw_needed", self._redraw) + widget:weak_connect_signal("widget::layout_changed", self._layout) + widget:weak_connect_signal("widget::emit_recursive", self._emit_recursive) + end + + -- Update children + local old_children = self._children + local layout_result = base.layout_widget(no_parent, context, widget, width, height) + self._children = {} + for _, w in ipairs(layout_result or {}) do + local r = table.remove(old_children, 1) + if not r then + r = hierarchy_new(self._redraw_callback, self._layout_callback, self._callback_arg) + r._parent = self + end + hierarchy_update(r, context, w._widget, w._width, w._height, region, w._matrix, w._matrix * matrix_to_device) + table.insert(self._children, r) + end + + -- Calculate the draw extents + local x1, y1, x2, y2 = 0, 0, width, height + if not widget.clip_child_extends then + for _, h in ipairs(self._children) do + local px, py, pwidth, pheight = matrix.transform_rectangle(h._matrix, h:get_draw_extents()) + x1 = math.min(x1, px) + y1 = math.min(y1, py) + x2 = math.max(x2, px + pwidth) + y2 = math.max(y2, py + pheight) + end + end + self._draw_extents = { + x = x1, y = y1, + width = x2 - x1, + height = y2 - y1 + } + + -- Update widget counts + self._widget_counts = {} + if widgets_to_count[widget] and width > 0 and height > 0 then + self._widget_counts[widget] = 1 + end + for _, h in ipairs(self._children) do + for w, count in pairs(h._widget_counts) do + self._widget_counts[w] = (self._widget_counts[w] or 0) + count + end + end + + -- Check which part needs to be redrawn + + -- Are there any children which were removed? Their area needs a redraw. + for _, child in ipairs(old_children) do + local x, y, w, h = matrix.transform_rectangle(child._matrix_to_device, child:get_draw_extents()) + region:union_rectangle(cairo.RectangleInt { + x = x, y = y, width = w, height = h + }) + child._parent = nil + end + + -- Did we change and need to be redrawn? + local x, y, w, h = matrix.transform_rectangle(self._matrix_to_device, 0, 0, self._size.width, self._size.height) + local new_x, new_y = math.floor(x), math.floor(y) + local new_width, new_height = math.ceil(x + w) - new_x, math.ceil(y + h) - new_y + if new_x ~= old_x or new_y ~= old_y or new_width ~= old_width or new_height ~= old_height or + widget ~= old_widget then + region:union_rectangle(cairo.RectangleInt { + x = old_x, y = old_y, width = old_width, height = old_height + }) + region:union_rectangle(cairo.RectangleInt { + x = new_x, y = new_y, width = new_width, height = new_height + }) + end +end + +--- Create a new widget hierarchy that has no parent. +-- @param context The context in which we are laid out. +-- @param widget The widget that is at the base of the hierarchy. +-- @param width The available width for this hierarchy. +-- @param height The available height for this hierarchy. +-- @param redraw_callback Callback that is called with the corresponding widget +-- hierarchy on widget::redraw_needed on some widget. +-- @param layout_callback Callback that is called with the corresponding widget +-- hierarchy on widget::layout_changed on some widget. +-- @param callback_arg A second argument that is given to the above callbacks. +-- @return A new widget hierarchy +-- @constructorfct wibox.hierarchy.new +function hierarchy.new(context, widget, width, height, redraw_callback, layout_callback, callback_arg) + local result = hierarchy_new(redraw_callback, layout_callback, callback_arg) + result:update(context, widget, width, height) + return result +end + +--- Update a widget hierarchy with some new state. +-- @param context The context in which we are laid out. +-- @param widget The widget that is at the base of the hierarchy. +-- @param width The available width for this hierarchy. +-- @param height The available height for this hierarchy. +-- @param[opt] region A region to use for accumulating changed parts +-- @return A cairo region describing the changed parts (either the `region` +-- argument or a new, internally created region). +-- @method update +function hierarchy:update(context, widget, width, height, region) + region = region or cairo.Region.create() + hierarchy_update(self, context, widget, width, height, region, self._matrix, self._matrix_to_device) + return region +end + +--- Get the widget that this hierarchy manages. +-- @method get_widget +function hierarchy:get_widget() + return self._widget +end + +--- Get a matrix that transforms to the parent's coordinate space from this +-- hierarchy's coordinate system. +-- @return A matrix describing the transformation. +-- @method get_matrix_to_parent +function hierarchy:get_matrix_to_parent() + return self._matrix +end + +--- Get a matrix that transforms to the base of this hierarchy's coordinate +-- system (aka the coordinate system of the device that this +-- hierarchy is applied upon) from this hierarchy's coordinate system. +-- @return A matrix describing the transformation. +-- @method get_matrix_to_device +function hierarchy:get_matrix_to_device() + return self._matrix_to_device +end + +--- Get a matrix that transforms from the parent's coordinate space into this +-- hierarchy's coordinate system. +-- @return A matrix describing the transformation. +-- @method get_matrix_from_parent +function hierarchy:get_matrix_from_parent() + local m = self:get_matrix_to_parent() + return m:invert() +end + +--- Get a matrix that transforms from the base of this hierarchy's coordinate +-- system (aka the coordinate system of the device that this +-- hierarchy is applied upon) into this hierarchy's coordinate system. +-- @return A matrix describing the transformation. +-- @method get_matrix_from_device +function hierarchy:get_matrix_from_device() + local m = self:get_matrix_to_device() + return m:invert() +end + +--- Get the extents that this hierarchy possibly draws to (in the current coordinate space). +-- This includes the size of this element plus the size of all children +-- (after applying the corresponding transformation). +-- @return x, y, width, height +-- @method get_draw_extents +function hierarchy:get_draw_extents() + local ext = self._draw_extents + return ext.x, ext.y, ext.width, ext.height +end + +--- Get the size that this hierarchy logically covers (in the current coordinate space). +-- @return width, height +-- @method get_size +function hierarchy:get_size() + local ext = self._size + return ext.width, ext.height +end + +--- Get a list of all children. +-- @return List of all children hierarchies. +-- @method get_children +function hierarchy:get_children() + return self._children +end + +--- Count how often this widget is visible inside this hierarchy. This function +-- only works with widgets registered via `count_widget`. +-- @param widget The widget that should be counted +-- @return The number of times that this widget is contained in this hierarchy. +-- @method get_count +function hierarchy:get_count(widget) + return self._widget_counts[widget] or 0 +end + +--- Does the given cairo context have an empty clip (aka "no drawing possible")? +local function empty_clip(cr) + local x1, y1, x2, y2 = cr:clip_extents() + return x2 - x1 == 0 or y2 - y1 == 0 +end + +--- Draw a hierarchy to some cairo context. +-- This function draws the widgets in this widget hierarchy to the given cairo +-- context. The context's clip is used to skip parts that aren't visible. +-- @param context The context in which widgets are drawn. +-- @param cr The cairo context that is used for drawing. +-- @method draw +function hierarchy:draw(context, cr) + local widget = self:get_widget() + if not widget._private.visible then + return + end + + cr:save() + cr:transform(self:get_matrix_to_parent():to_cairo_matrix()) + + -- Clip to the draw extents + cr:rectangle(self:get_draw_extents()) + cr:clip() + + -- Draw if needed + if not empty_clip(cr) then + local opacity = widget:get_opacity() + local function call(func, extra_arg1, extra_arg2) + if not func then return end + if not extra_arg2 then + protected_call(func, widget, context, cr, self:get_size()) + else + protected_call(func, widget, context, extra_arg1, extra_arg2, cr, self:get_size()) + end + end + + -- Prepare opacity handling + if opacity ~= 1 then + cr:push_group() + end + + -- Draw the widget + cr:save() + cr:rectangle(0, 0, self:get_size()) + cr:clip() + call(widget.draw) + cr:restore() + -- Clear any path that the widget might have left + cr:new_path() + + -- Draw its children (We already clipped to the draw extents above) + call(widget.before_draw_children) + for i, wi in ipairs(self:get_children()) do + call(widget.before_draw_child, i, wi:get_widget()) + wi:draw(context, cr) + call(widget.after_draw_child, i, wi:get_widget()) + end + call(widget.after_draw_children) + -- Clear any path that the widget might have left + cr:new_path() + + -- Apply opacity + if opacity ~= 1 then + cr:pop_group_to_source() + cr.operator = cairo.Operator.OVER + cr:paint_with_alpha(opacity) + end + end + + cr:restore() +end + +return hierarchy diff --git a/awesome/wibox/layout/fixed.lua b/awesome/wibox/layout/fixed.lua new file mode 100644 index 0000000..085b44a --- /dev/null +++ b/awesome/wibox/layout/fixed.lua @@ -0,0 +1,482 @@ +--------------------------------------------------------------------------- +-- A `fixed` layout may be initialized with any number of child widgets, and +-- during runtime widgets may be added and removed dynamically. +-- +-- On the main axis, child widgets are given a fixed size of exactly as much +-- space as they ask for. The layout will then resize according to the sum of +-- all child widgets. If the space available to the layout is not enough to +-- include all child widgets, the excessive ones are not drawn at all. +-- +-- Additionally, the layout allows adding empty spacing or even placing a custom +-- spacing widget between the child widget. +-- +-- On its secondary axis, the layout's size is determined by the largest child +-- widget. Smaller child widgets are then placed with the same size. +-- Therefore, child widgets may ignore their `forced_width` or `forced_height` +-- properties for vertical and horizontal layouts respectively. +-- +--@DOC_wibox_layout_defaults_fixed_EXAMPLE@ +-- +-- @author Uli Schlachter +-- @copyright 2010 Uli Schlachter +-- @layoutmod wibox.layout.fixed +-- @supermodule wibox.widget.base +--------------------------------------------------------------------------- + +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 gtable = require("gears.table") + +local fixed = {} + +-- Layout a fixed layout. Each widget gets just the space it asks for. +-- @param context The context in which we are drawn. +-- @param width The available width. +-- @param height The available height. +function fixed:layout(context, width, height) + local result = {} + local spacing = self._private.spacing or 0 + local is_y = self._private.dir == "y" + local is_x = not is_y + local abspace = math.abs(spacing) + local spoffset = spacing < 0 and 0 or spacing + local widgets_nr = #self._private.widgets + local spacing_widget + local x, y = 0, 0 + + spacing_widget = spacing ~= 0 and self._private.spacing_widget or nil + + for index, widget in pairs(self._private.widgets) do + local w, h, local_spacing = width - x, height - y, spacing + + -- Some widget might be zero sized either because this is their + -- minimum space or just because they are really empty. In this case, + -- they must still be added to the layout. Otherwise, if their size + -- change and this layout is resizable, they are lost "forever" until + -- a full relayout is called on this fixed layout object. + local zero = false + + if is_y then + if index ~= widgets_nr or not self._private.fill_space then + h = select(2, base.fit_widget(self, context, widget, w, h)) + zero = h == 0 + end + + if y - spacing >= height then + -- pop the spacing widget added in previous iteration if used + if spacing_widget then + table.remove(result) + + -- Avoid adding zero-sized widgets at an out-of-bound + -- position. + y = y - spacing + end + + -- Never display "random" widgets as soon as a non-zero sized + -- one doesn't fit. + if not zero then + break + end + end + else + if index ~= widgets_nr or not self._private.fill_space then + w = select(1, base.fit_widget(self, context, widget, w, h)) + zero = w == 0 + end + + if x - spacing >= width then + -- pop the spacing widget added in previous iteration if used + if spacing_widget then + table.remove(result) + + -- Avoid adding zero-sized widgets at an out-of-bound + -- position. + x = x - spacing + end + + -- Never display "random" widgets as soon as a non-zero sized + -- one doesn't fit. + if not zero then + break + end + end + end + + if zero then + local_spacing = 0 + end + + -- Place widget, even if it has zero width/height. Otherwise + -- any layout change for zero-sized widget would become invisible. + table.insert(result, base.place_widget_at(widget, x, y, w, h)) + + x = is_x and x + w + local_spacing or x + y = is_y and y + h + local_spacing or y + + -- Add the spacing widget (if needed) + if index < widgets_nr and spacing_widget then + table.insert(result, base.place_widget_at( + spacing_widget, + is_x and (x - spoffset) or x, + is_y and (y - spoffset) or y, + is_x and abspace or w, + is_y and abspace or h + )) + end + end + + return result +end + +--- Add some widgets to the given layout. +-- +-- @method add +-- @tparam widget ... Widgets that should be added (must at least be one). +-- @interface layout +function fixed:add(...) + -- No table.pack in Lua 5.1 :-( + local args = { n = select('#', ...), ... } + assert(args.n > 0, "need at least one widget to add") + for i = 1, args.n do + local w = base.make_widget_from_value(args[i]) + base.check_widget(w) + table.insert(self._private.widgets, w) + end + self:emit_signal("widget::layout_changed") +end + +--- Remove a widget from the layout. +-- +-- @method remove +-- @tparam number index The widget index to remove +-- @treturn boolean index If the operation is successful +-- @interface layout +function fixed:remove(index) + if not index or index < 1 or index > #self._private.widgets then return false end + + table.remove(self._private.widgets, index) + + 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. +-- @method remove_widgets +-- @tparam widget ... Widgets that should be removed (must at least be one) +-- @treturn boolean If the operation is successful +-- @interface layout +function fixed: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 fixed:get_children() + return self._private.widgets +end + +function fixed: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`. +-- @method replace_widget +-- @tparam widget widget The widget to replace +-- @tparam widget 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 +-- @interface layout +function fixed: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 fixed:swap(index1, index2) + if not index1 or not index2 or index1 > #self._private.widgets + or index2 > #self._private.widgets then + return false + end + + local widget1, widget2 = self._private.widgets[index1], self._private.widgets[index2] + + self:set(index1, widget2) + self:set(index2, widget1) + + self:emit_signal("widget::swapped", widget1, widget2, index2, index1) + + return true +end + +function fixed: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) + if l1 == self then + self:emit_signal("widget::swapped", widget1, widget2, idx2, idx1) + end + elseif l1.set_widget then + l1:set_widget(widget2) + end + if l2.set then + l2:set(idx2, widget1) + if l2 == self then + self:emit_signal("widget::swapped", widget1, widget2, idx2, idx1) + end + elseif l2.set_widget then + l2:set_widget(widget1) + end + + return true + end + + return false +end + +function fixed:set(index, widget2) + if (not widget2) or (not self._private.widgets[index]) then return false end + + base.check_widget(widget2) + + local w = self._private.widgets[index] + + self._private.widgets[index] = widget2 + + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::replaced", widget2, w, index) + + return true +end + +--- A widget to insert as a separator between child widgets. +-- +-- If this property is a valid widget and `spacing` is greater than `0`, a +-- copy of this widget is inserted between each child widget, with its size in +-- the layout's main direction determined by `spacing`. +-- +-- By default no widget is used and any `spacing` is applied as an empty offset. +-- +--@DOC_wibox_layout_fixed_spacing_widget_EXAMPLE@ +-- +-- @property spacing_widget +-- @tparam widget spacing_widget +-- @propemits true false +-- @interface layout + +function fixed:set_spacing_widget(wdg) + self._private.spacing_widget = base.make_widget_from_value(wdg) + self:emit_signal("widget::layout_changed") + self:emit_signal("property::spacing_widget", wdg) +end + +--- Insert a new widget in the layout at position `index`. +-- +-- @method insert +-- @tparam number index The position. +-- @tparam widget widget The widget. +-- @treturn boolean If the operation is successful. +-- @emits widget::inserted +-- @emitstparam widget::inserted widget self The fixed layout. +-- @emitstparam widget::inserted widget widget index The inserted widget. +-- @emitstparam widget::inserted number count The widget count. +-- @interface layout +function fixed:insert(index, widget) + if not index or index < 1 or index > #self._private.widgets + 1 then return false end + + base.check_widget(widget) + table.insert(self._private.widgets, index, widget) + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::inserted", widget, #self._private.widgets) + + return true +end + +-- Fit the fixed 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 fixed:fit(context, orig_width, orig_height) + local widgets = self._private.widgets + local num_widgets = #widgets + -- Return early if there are no widgets to draw + if num_widgets < 1 then + return 0, 0 + end + + local width_left, height_left = orig_width, orig_height + local used_in_dir, used_max = 0, 0 + local is_y = self._private.dir == "y" + local spacing = self._private.spacing or 0 + + for k, v in ipairs(widgets) do + -- Keep in mind that `spacing` may be negative to create overlap + if k > 1 and spacing ~= 0 then + used_in_dir = used_in_dir + spacing + + if is_y then + height_left = height_left - spacing + else + width_left = width_left - spacing + end + end + + local w, h = base.fit_widget(self, context, v, width_left, height_left) + + + -- Determine if the widget still fits + local is_enough + + if is_y then + is_enough = height_left >= h + + if is_enough then + used_max = math.max(used_max, w) + used_in_dir = used_in_dir + h + height_left = height_left - h + end + else + is_enough = w > 0 and width_left >= w + + if is_enough then + used_max = math.max(used_max, h) + used_in_dir = used_in_dir + w + width_left = width_left - w + end + end + + if not is_enough then + -- Remove previous spacing if there was not enough space + -- to add the current widget + used_in_dir = used_in_dir - spacing + break + elseif width_left <= 0 or height_left <= 0 then + break + end + end + + if is_y then + return used_max, used_in_dir + else + return used_in_dir, used_max + end +end + +function fixed:reset() + self._private.widgets = {} + self:emit_signal("widget::layout_changed") + self:emit_signal("widget::reseted") + self:emit_signal("widget::reset") +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 +-- @tparam boolean fill_space +-- @propemits true false + +function fixed:fill_space(val) + if self._private.fill_space ~= val then + self._private.fill_space = not not val + self:emit_signal("widget::layout_changed") + self:emit_signal("property::fill_space", val) + end +end + +local function get_layout(dir, widget1, ...) + local ret = base.make_widget(nil, nil, { enable_properties = true }) + + gtable.crush(ret, fixed, true) + + ret._private.dir = dir + ret._private.widgets = {} + ret:set_spacing(0) + ret:fill_space(false) + + if widget1 then + ret:add(widget1, ...) + end + + return ret +end + +--- Creates and returns a new horizontal fixed layout. +-- +-- @tparam widget ... Widgets that should be added to the layout. +-- @constructorfct wibox.layout.fixed.horizontal +function fixed.horizontal(...) + return get_layout("x", ...) +end + +--- Creates and returns a new vertical fixed layout. +-- +-- @tparam widget ... Widgets that should be added to the layout. +-- @constructorfct wibox.layout.fixed.vertical +function fixed.vertical(...) + return get_layout("y", ...) +end + +--- The amount of space inserted between the child widgets. +-- +-- If a `spacing_widget` is defined, this value is used for its size. +-- +--@DOC_wibox_layout_fixed_spacing_EXAMPLE@ +-- +-- @property spacing +-- @tparam number spacing Spacing between widgets. +-- @propemits true false +-- @interface layout + +function fixed:set_spacing(spacing) + if self._private.spacing ~= spacing then + self._private.spacing = spacing + self:emit_signal("widget::layout_changed") + self:emit_signal("property::spacing", spacing) + end +end + +function fixed:get_spacing() + return self._private.spacing or 0 +end + +--@DOC_fixed_COMMON@ + +return fixed + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80