--------------------------------------------------------------------------- -- An interactive mouse based slider widget. -- -- @author Grigory Mishchenko <grishkokot@gmail.com> -- @author Emmanuel Lepage Vallee <elv1313@gmail.com> -- @copyright 2015 Grigory Mishchenko, 2016 Emmanuel Lepage Vallee -- @release @AWESOME_VERSION@ -- @classmod wibox.widget.slider --------------------------------------------------------------------------- local setmetatable = setmetatable local type = type local color = require("gears.color") local util = require("awful.util") local beautiful = require("beautiful") local base = require("wibox.widget.base") local shape = require("gears.shape") local gmatrix = require("gears.matrix") local capi = { mouse = mouse, mousegrabber = mousegrabber, root = root, } local slider = {mt={}} local properties = { -- Handle handle_shape = shape.rectangle, handle_color = false, handle_margins = {}, handle_width = false, handler_border_width = 0, handler_border_color = false, -- Bar bar_shape = shape.rectangle, bar_height = false, bar_color = false, bar_margins = {}, bar_border_width = 0, bar_border_color = false, -- Content value = 0, minimum = 0, maximum = 100, -- Other cursor = "fleur", } -- Create the accessors for prop in pairs(properties) do slider["set_"..prop] = function(self, value) local changed = self._private[prop] ~= value self._private[prop] = value if changed then self:emit_signal("property::"..prop) self:emit_signal("widget::redraw_needed") end end slider["get_"..prop] = function(self) -- Ignoring the false's is on purpose return self._private[prop] == nil and properties[prop] or self._private[prop] end end -- Add some validation to set_value function slider:set_value(value) value = math.min(value, self:get_maximum()) value = math.max(value, self:get_minimum()) local changed = self._private.value ~= value self._private.value = value if changed then self:emit_signal( "property::value" ) self:emit_signal( "widget::redraw_needed" ) end end local function get_extremums(self) local min = self._private.minimum or properties.minimum local max = self._private.maximum or properties.maximum local interval = max - min return min, max, interval end local function get_value(self, width, x) local _, _, interval = get_extremums(self) return math.floor((x*interval)/width) end function slider:draw(_, cr, width, height) local bar_height = self._private.bar_height -- If there is no background, then skip this local bar_color = self._private.bar_color or beautiful.slider_bar_color if bar_color then cr:set_source(color(bar_color)) end local margins = self._private.bar_margins or beautiful.slider_bar_margins local x_offset, right_margin, y_offset = 0, 0 if margins then if type(margins) == "number" then bar_height = bar_height or (height - 2*margins) x_offset, y_offset = margins, margins right_margin = margins else bar_height = bar_height or (height - margins.top - margins.bottom) x_offset, y_offset = margins.left, margins.top right_margin = margins.right end else bar_height = bar_height or beautiful.slider_bar_height or height y_offset = (height - bar_height)/2 end cr:translate(x_offset, y_offset) local bar_shape = self._private.bar_shape or beautiful.slider_bar_shape or properties.bar_shape local bar_border_width = self._private.bar_border_width or beautiful.slider_bar_border_width or properties.bar_border_width bar_shape(cr, width - x_offset - right_margin, bar_height or height) if bar_color then if bar_border_width == 0 then cr:fill() else cr:fill_preserve() end end -- Draw the bar border if bar_border_width > 0 then local bar_border_color = self._private.bar_border_color or beautiful.slider_bar_border_color or properties.bar_border_color if bar_border_color then cr:save() cr:set_source(color(bar_border_color)) cr:stroke() cr:restore() else cr:stroke() end end cr:translate(-x_offset, -y_offset) -- Paint the handle local handle_color = self._private.handle_color or beautiful.slider_handle_color -- It is ok if there is no color, it will be inherited if handle_color then cr:set_source(color(handle_color)) end local handle_height, handle_width = height, self._private.handle_width or beautiful.slider_handle_width or height/2 local handle_shape = self._private.handle_shape or beautiful.slider_handle_shape or properties.handle_shape -- Lets get the margins for the handle local margins = self._private.bar_margins or beautiful.slider_bar_margins local x_offset, y_offset = 0, 0 if margins then if type(margins) == "number" then x_offset, y_offset = margins, margins handle_width = handle_width - 2*margins handle_height = handle_height - 2*margins else x_offset, y_offset = margins.left, margins.top handle_width = handle_width - margins.left - margins.right handle_height = handle_height - margins.top - margins.bottom end end local value = self._private.value or self._private.min or 0 -- Get the widget size back to it's non-transfored value --local matrix = gmatrix.from_cairo_matrix(cr:get_matrix()) local min, _, interval = get_extremums(self) local rel_value = ((value-min)/interval) * (width-handle_width) cr:translate(x_offset + rel_value, y_offset) local handle_border_width = self._private.handle_border_width or beautiful.slider_handle_border_width or properties.handle_border_width handle_shape(cr, handle_width, handle_height) if handle_border_width > 0 then cr:fill_preserve() else cr:fill() end -- Draw the handle border if handle_border_width > 0 then local handle_border_color = self._private.handle_border_color or beautiful.slider_handle_border_color or properties.handle_border_color if handle_border_color then cr:set_source(color(handle_border_color)) end cr:stroke() end end function slider:fit(_, width, height) -- Use all the space, this should be used with a constraint widget return width, height end local event = {} function event.mouse_enter(self) self:emit_signal("widget::redraw_needed") --TODO local cursor = self._private.cursor or "fleur" capi.root.cursor(cursor) end function event.mouse_leave(self) self:emit_signal("widget::redraw_needed") --TODO -- capi.root.cursor("fleur") end local rect_order = {"x", "y", "width", "height"} -- Move the handle to the correct location local function move_handle(self, x, y, geo) -- Fix the X position when used in a rotate or mirror widget -- Remove the translation to bring {x, y} and the rect on the same origin local rem_translate = geo.matrix_to_device*gmatrix.identity rem_translate.x0, rem_translate.y0 = 0, 0 local rect = {rem_translate:transform_rectangle( 0, 0, geo.width, geo.height )} -- Floating point precision is here both too much and not enough for k, v in ipairs(rect) do rect[rect_order[k]] = util.round(v) end local px = rem_translate:transform_point(x, y) -- If the transformation moved the rectangle away from {0,0}, compensate if rect.x < 0 then px = rect.width + px end -- --HACK Cairo 0,0 is top left, a matrix is bottom left, funny... if util.round(rem_translate:transform_point(0, 100)) ~= 0 then px = -(px - rect.width) end self:set_value(get_value(self, rect.width, px)) self:emit_signal("widget::redraw_needed") end function event.mouse_press(self, x, y, button_id, _, geo) if button_id ~= 1 then return end move_handle(self, x, y, geo) -- Get the "real" position of the widget local wgeo = geo.drawable.drawable:geometry() local wx = wgeo.x + geo.x local wy = wgeo.y + geo.y capi.mousegrabber.run(function(mouse) if not mouse.buttons[1] then self:emit_signal("widget::redraw_needed") return false end -- Get the closest point to the widget local abs_x, abs_y = mouse.x, mouse.y abs_x = abs_x > (wx+geo.width) and wx+geo.width or ( abs_x < wx and wx ) or abs_x abs_y = abs_y > (wy+geo.height) and wy+geo.height or ( abs_y < wy and wy ) or abs_y -- Get the relative point local rx, ry = abs_x - wx, abs_y - wy move_handle(self, rx, ry, geo) return true end, self:get_cursor()) end --- Create a slider widget. -- @tparam[opt={}] table args -- @function wibox.widget.slider local function new(args) local ret = base.make_widget(nil, nil, { enable_properties = true, }) util.table.crush(ret._private, args or {}) util.table.crush(ret, slider, true) -- Change the cusor and redraw (in case there is hover and pressed colors) ret:connect_signal("mouse::enter" , event.mouse_enter) ret:connect_signal("mouse::leave" , event.mouse_leave) ret:connect_signal("button::press", event.mouse_press) return ret end function slider.mt:__call(...) return new(...) end --FIXME This is still broken... --[[w:setup{ { { widget = wibox.widget.slider, }, direction = "east", widget = wibox.container.rotate }, reflection = { horizontal = true }, widget = wibox.container.mirror }]] return setmetatable(slider, slider.mt) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80