diff --git a/docs/aliases/awful_mouse.lua b/docs/aliases/awful_mouse.lua new file mode 100644 index 00000000..9b339a6f --- /dev/null +++ b/docs/aliases/awful_mouse.lua @@ -0,0 +1,6 @@ +--------------------------------------------------------------------------- +--- This module is deprecated, use `mouse` +-- =============================== +-- +-- @module awful.mouse +--------------------------------------------------------------------------- diff --git a/docs/config.ld b/docs/config.ld index b220b720..19886170 100644 --- a/docs/config.ld +++ b/docs/config.ld @@ -35,6 +35,8 @@ new_type("function", "Functions") new_type("property", "Object properties", false, "Type") -- New type for signals new_type("signal", "Signals", false, "Arguments") +-- New type for signals connections +new_type("signalhandler", "Request handlers", false, "Arguments") -- Allow objects to define a set of beautiful properties affecting them new_type("beautiful", "Theme variables", false, "Type") -- Put deprecated methods in their own section @@ -67,6 +69,7 @@ file = { '../docs/aliases/awful_client.lua', '../docs/aliases/awful_screen.lua', '../docs/aliases/awful_tag.lua', + '../docs/aliases/awful_mouse.lua', exclude = { -- exclude these modules, as they do not contain any written -- documentation diff --git a/docs/images/mouse.svg b/docs/images/mouse.svg new file mode 100644 index 00000000..e56eff51 --- /dev/null +++ b/docs/images/mouse.svg @@ -0,0 +1,253 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + Button 1(LMB) + Button 2(RMB) + + + Click: Button 3 + Down: Button 5 + Up: Button 4 + + + + + + + + + + + + + + + diff --git a/lib/awful/ewmh.lua b/lib/awful/ewmh.lua index ffe04fd7..d110cd31 100644 --- a/lib/awful/ewmh.lua +++ b/lib/awful/ewmh.lua @@ -15,6 +15,7 @@ local math = math local util = require("awful.util") local aclient = require("awful.client") local aplace = require("awful.placement") +local asuit = require("awful.layout.suit") local ewmh = {} @@ -100,6 +101,7 @@ end -- -- It is the default signal handler for `request::activate` on a `client`. -- +-- @signalhandler awful.ewmh.activate -- @client c A client to use -- @tparam string context The context where this signal was used. -- @tparam[opt] table hints A table with additional hints: @@ -120,8 +122,11 @@ function ewmh.activate(c, context, hints) -- luacheck: no unused args end end ---- Tag a window with its requested tag +--- Tag a window with its requested tag. -- +-- It is the default signal handler for `request::tag` on a `client`. +-- +-- @signalhandler awful.ewmh.tag -- @client c A client to tag -- @tag[opt] t A tag to use. If omitted, then the client is made sticky. -- @tparam[opt={}] table hints Extra information @@ -156,10 +161,18 @@ local context_mapper = { -- -- This is the default geometry request handler. -- +-- @signalhandler awful.ewmh.geometry -- @tparam client c The client -- @tparam string context The context -- @tparam[opt={}] table hints The hints to pass to the handler function ewmh.geometry(c, context, hints) + local layout = c.screen.selected_tag and c.screen.selected_tag.layout or nil + + -- Setting the geometry wont work unless the client is floating. + if (not c.floating) and (not layout == asuit.floating) then + return + end + context = context or "" local original_context = context diff --git a/lib/awful/layout/init.lua b/lib/awful/layout/init.lua index 482bab63..df99b1b0 100644 --- a/lib/awful/layout/init.lua +++ b/lib/awful/layout/init.lua @@ -246,6 +246,28 @@ capi.client.connect_signal("list", function() end end) +--- Default handler for tiled clients request::geometry with the `mouse.move` +-- context. +-- @tparam client c The client +-- @tparam string context The context +-- @tparam table hints Additional hints +function layout.move_handler(c, context, hints) --luacheck: no unused args + -- Quit if it isn't a mouse.move on a tiled layout, that's handled elsewhere + if c.floating then return end + if context ~= "mouse.move" then return end + local l = c.screen.selected_tag and c.screen.selected_tag.layout or nil + if l == layout.suit.floating then return end + + local c_u_m = capi.mouse.current_client + if c_u_m and not c_u_m.floating then + if c_u_m ~= c then + c:swap(c_u_m) + end + end +end + +capi.client.connect_signal("request::geometry", layout.move_handler) + return layout -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/mouse/drag_to_tag.lua b/lib/awful/mouse/drag_to_tag.lua new file mode 100644 index 00000000..ee4cb895 --- /dev/null +++ b/lib/awful/mouse/drag_to_tag.lua @@ -0,0 +1,59 @@ +--------------------------------------------------------------------------- +--- When the the mouse reach the end of the screen, then switch tag instead +-- of screens. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @release @AWESOME_VERSION@ +-- @submodule mouse +--------------------------------------------------------------------------- + +local capi = {screen = screen, mouse = mouse} +local util = require("awful.util") +local tag = require("awful.tag") +local resize = require("awful.mouse.resize") + +local module = {} + +function module.drag_to_tag(c) + if (not c) or (not c.valid) then return end + + local coords = capi.mouse.coords() + + local dir = nil + + local wa = capi.screen[c.screen].workarea + + if coords.x >= wa.x + wa.width - 1 then + capi.mouse.coords({ x = wa.x + 2 }, true) + dir = "right" + elseif coords.x <= wa.x + 1 then + capi.mouse.coords({ x = wa.x + wa.width - 2 }, true) + dir = "left" + end + + local tags = c.screen.tags + local t = c.screen.selected_tag + local idx = t.index + + if dir then + + if dir == "right" then + local newtag = tags[util.cycle(#tags, idx + 1)] + c:move_to_tag(newtag) + tag.viewnext() + elseif dir == "left" then + local newtag = tags[util.cycle(#tags, idx - 1)] + c:move_to_tag(newtag) + tag.viewprev() + end + end +end + +resize.add_move_callback(function(c, _, _) + if module.enabled then + module.drag_to_tag(c) + end +end, "mouse.move") + +return setmetatable(module, {__call = function(_, ...) return module.drag_to_tag(...) end}) diff --git a/lib/awful/mouse/init.lua b/lib/awful/mouse/init.lua index 7a8a86a7..13cd23ae 100644 --- a/lib/awful/mouse/init.lua +++ b/lib/awful/mouse/init.lua @@ -4,17 +4,15 @@ -- @author Julien Danjou <julien@danjou.info> -- @copyright 2008 Julien Danjou -- @release @AWESOME_VERSION@ --- @module awful.mouse +-- @module mouse --------------------------------------------------------------------------- -- Grab environment we need local layout = require("awful.layout") -local tag = require("awful.tag") -local aclient = require("awful.client") +local aplace = require("awful.placement") local awibox = require("awful.wibox") local util = require("awful.util") local type = type -local math = math local ipairs = ipairs local capi = { @@ -25,136 +23,63 @@ local capi = mousegrabber = mousegrabber, } -local mouse = {} +local mouse = { + resize = require("awful.mouse.resize"), + snap = require("awful.mouse.snap"), + drag_to_tag = require("awful.mouse.drag_to_tag") +} +mouse.object = {} mouse.client = {} mouse.wibox = {} +--- The default snap distance. +-- @tfield integer awful.mouse.snap.default_distance +-- @tparam[opt=8] integer default_distance +-- @see awful.mouse.snap + +--- Enable screen edges snapping. +-- @tfield[opt=true] boolean awful.mouse.snap.edge_enabled + +--- Enable client to client snapping. +-- @tfield[opt=true] boolean awful.mouse.snap.client_enabled + +--- Enable changing tag when a client is dragged to the edge of the screen. +-- @tfield[opt=false] integer awful.mouse.drag_to_tag.enabled + +--- The snap outline background color. +-- @beautiful beautiful.snap_bg +-- @tparam color|string|gradient|pattern color + +--- The snap outline width. +-- @beautiful beautiful.snap_border_width +-- @param integer + +--- The snap outline shape. +-- @beautiful beautiful.snap_shape +-- @tparam function shape A `gears.shape` compatible function + --- Get the client object under the pointer. +-- @deprecated awful.mouse.client_under_pointer -- @return The client object under the pointer, if one can be found. +-- @see current_client function mouse.client_under_pointer() - local obj = capi.mouse.object_under_pointer() - if type(obj) == "client" then - return obj - end -end + util.deprecated("Use mouse.current_client instead of awful.mouse.client_under_pointer()") ---- Get the drawin object under the pointer. --- @return The drawin object under the pointer, if one can be found. -function mouse.drawin_under_pointer() - local obj = capi.mouse.object_under_pointer() - if type(obj) == "drawin" then - return obj - end -end - -local function snap_outside(g, sg, snap) - if g.x < snap + sg.x + sg.width and g.x > sg.x + sg.width then - g.x = sg.x + sg.width - elseif g.x + g.width < sg.x and g.x + g.width > sg.x - snap then - g.x = sg.x - g.width - end - if g.y < snap + sg.y + sg.height and g.y > sg.y + sg.height then - g.y = sg.y + sg.height - elseif g.y + g.height < sg.y and g.y + g.height > sg.y - snap then - g.y = sg.y - g.height - end - return g -end - -local function snap_inside(g, sg, snap) - local edgev = 'none' - local edgeh = 'none' - if math.abs(g.x) < snap + sg.x and g.x > sg.x then - edgev = 'left' - g.x = sg.x - elseif math.abs((sg.x + sg.width) - (g.x + g.width)) < snap then - edgev = 'right' - g.x = sg.x + sg.width - g.width - end - if math.abs(g.y) < snap + sg.y and g.y > sg.y then - edgeh = 'top' - g.y = sg.y - elseif math.abs((sg.y + sg.height) - (g.y + g.height)) < snap then - edgeh = 'bottom' - g.y = sg.y + sg.height - g.height - end - - -- What is the dominant dimension? - if g.width > g.height then - return g, edgeh - else - return g, edgev - end -end - ---- Snap a client to the closest client or screen edge. --- @param c The client to snap. --- @param snap The pixel to snap clients. --- @param x The client x coordinate. --- @param y The client y coordinate. --- @param fixed_x True if the client isn't allowed to move in the x direction. --- @param fixed_y True if the client isn't allowed to move in the y direction. -function mouse.client.snap(c, snap, x, y, fixed_x, fixed_y) - snap = snap or 8 - c = c or capi.client.focus - local cur_geom = c:geometry() - local geom = c:geometry() - geom.width = geom.width + (2 * c.border_width) - geom.height = geom.height + (2 * c.border_width) - local edge - geom.x = x or geom.x - geom.y = y or geom.y - - geom, edge = snap_inside(geom, capi.screen[c.screen].geometry, snap) - geom = snap_inside(geom, capi.screen[c.screen].workarea, snap) - - -- Allow certain windows to snap to the edge of the workarea. - -- Only allow docking to workarea for consistency/to avoid problems. - if c.dockable then - local struts = c:struts() - struts['left'] = 0 - struts['right'] = 0 - struts['top'] = 0 - struts['bottom'] = 0 - if edge ~= "none" and c.floating then - if edge == "left" or edge == "right" then - struts[edge] = cur_geom.width - elseif edge == "top" or edge == "bottom" then - struts[edge] = cur_geom.height - end - end - c:struts(struts) - end - - geom.x = geom.x - (2 * c.border_width) - geom.y = geom.y - (2 * c.border_width) - - for _, snapper in ipairs(aclient.visible(c.screen)) do - if snapper ~= c then - geom = snap_outside(geom, snapper:geometry(), snap) - end - end - - geom.width = geom.width - (2 * c.border_width) - geom.height = geom.height - (2 * c.border_width) - geom.x = geom.x + (2 * c.border_width) - geom.y = geom.y + (2 * c.border_width) - - -- It's easiest to undo changes afterwards if they're not allowed - if fixed_x then geom.x = cur_geom.x end - if fixed_y then geom.y = cur_geom.y end - - return geom + return mouse.object.get_current_client() end --- Move a client. +-- @function awful.mouse.client.move -- @param c The client to move, or the focused one if nil. -- @param snap The pixel to snap clients. --- @param finished_cb An optional callback function, that will be called --- when moving the client has been finished. The client --- that has been moved will be passed to that function. -function mouse.client.move(c, snap, finished_cb) +-- @param finished_cb Deprecated, do not use +function mouse.client.move(c, snap, finished_cb) --luacheck: no unused args + if finished_cb then + util.deprecated("The mouse.client.move `finished_cb` argument is no longer".. + " used, please use awful.mouse.resize.add_leave_callback(f, 'mouse.move')") + end + c = c or capi.client.focus if not c @@ -165,98 +90,39 @@ function mouse.client.move(c, snap, finished_cb) return end - local orig = c:geometry() - local m_c = capi.mouse.coords() - local dist_x = m_c.x - orig.x - local dist_y = m_c.y - orig.y - -- Only allow moving in the non-maximized directions - local fixed_x = c.maximized_horizontal - local fixed_y = c.maximized_vertical + -- Compute the offset + local coords = capi.mouse.coords() + local geo = aplace.centered(capi.mouse,{parent=c, pretend=true}) - capi.mousegrabber.run(function (_mouse) - if not c.valid then return false end + local offset = { + x = geo.x - coords.x, + y = geo.y - coords.y, + } - for _, v in ipairs(_mouse.buttons) do - if v then - local lay = layout.get(c.screen) - if lay == layout.suit.floating or c.floating then - local x = _mouse.x - dist_x - local y = _mouse.y - dist_y - c:geometry(mouse.client.snap(c, snap, x, y, fixed_x, fixed_y)) - elseif lay ~= layout.suit.magnifier then - -- Only move the client to the mouse - -- screen if the target screen is not - -- floating. - -- Otherwise, we move if via geometry. - if layout.get(capi.mouse.screen) == layout.suit.floating then - local x = _mouse.x - dist_x - local y = _mouse.y - dist_y - c:geometry(mouse.client.snap(c, snap, x, y, fixed_x, fixed_y)) - else - c.screen = capi.mouse.screen - end - if layout.get(c.screen) ~= layout.suit.floating then - local c_u_m = mouse.client_under_pointer() - if c_u_m and not c_u_m.floating then - if c_u_m ~= c then - c:swap(c_u_m) - end - end - end - end - return true - end - end - if finished_cb then - finished_cb(c) - end - return false - end, "fleur") + mouse.resize(c, "mouse.move", { + placement = aplace.under_mouse, + offset = offset, + snap = snap + }) end mouse.client.dragtotag = { } ---- Move a client to a tag by dragging it onto the left / right side of the screen +--- Move a client to a tag by dragging it onto the left / right side of the screen. +-- @deprecated awful.mouse.client.dragtotag.border -- @param c The client to move function mouse.client.dragtotag.border(c) - capi.mousegrabber.run(function (_mouse) - if not c.valid then return false end + util.deprecated("Use awful.mouse.snap.drag_to_tag_enabled = true instead ".. + "of awful.mouse.client.dragtotag.border(c). It will now be enabled.") - local button_down = false - for _, v in ipairs(_mouse.buttons) do - if v then button_down = true end - end - local wa = capi.screen[c.screen].workarea - if _mouse.x >= wa.x + wa.width then - capi.mouse.coords({ x = wa.x + wa.width - 1 }) - elseif _mouse.x <= wa.x then - capi.mouse.coords({ x = wa.x + 1 }) - end - if not button_down then - local tags = c.screen.tags - local t = c.screen.selected_tag - local idx - for i, v in ipairs(tags) do - if v == t then - idx = i - end - end - if _mouse.x > wa.x + wa.width - 10 then - local newtag = tags[util.cycle(#tags, idx + 1)] - c:move_to_tag(newtag) - tag.viewnext() - elseif _mouse.x < wa.x + 10 then - local newtag = tags[util.cycle(#tags, idx - 1)] - c:move_to_tag(newtag) - tag.viewprev() - end - return false - end - return true - end, "fleur") + -- Enable drag to border + mouse.snap.drag_to_tag_enabled = true + + return mouse.client.move(c) end ---- Move the wibox under the cursor +--- Move the wibox under the cursor. +-- @function awful.mouse.wibox.move --@param w The wibox to move, or none to use that under the pointer function mouse.wibox.move(w) w = w or mouse.wibox_under_pointer() @@ -297,55 +163,41 @@ function mouse.wibox.move(w) end --- Get a client corner coordinates. --- @param c The client to get corner from, focused one by default. --- @param corner The corner to use: auto, top_left, top_right, bottom_left, --- bottom_right. Default is auto, and auto find the nearest corner. --- @return Actual used corner and x and y coordinates. +-- @deprecated awful.mouse.client.corner +-- @tparam[opt=client.focus] client c The client to get corner from, focused one by default. +-- @tparam string corner The corner to use: auto, top_left, top_right, bottom_left, +-- bottom_right, left, right, top bottom. Default is auto, and auto find the +-- nearest corner. +-- @treturn string The corner name +-- @treturn number x The horizontal position +-- @treturn number y The vertical position function mouse.client.corner(c, corner) + util.deprecated( + "Use awful.placement.closest_corner(mouse) or awful.placement[corner](mouse)".. + " instead of awful.mouse.client.corner" + ) + c = c or capi.client.focus if not c then return end - local g = c:geometry() + local ngeo = nil - if not corner or corner == "auto" then - local m_c = capi.mouse.coords() - if math.abs(g.y - m_c.y) < math.abs(g.y + g.height - m_c.y) then - if math.abs(g.x - m_c.x) < math.abs(g.x + g.width - m_c.x) then - corner = "top_left" - else - corner = "top_right" - end - else - if math.abs(g.x - m_c.x) < math.abs(g.x + g.width - m_c.x) then - corner = "bottom_left" - else - corner = "bottom_right" - end - end + if (not corner) or corner == "auto" then + ngeo, corner = aplace.closest_corner(mouse, {parent = c}) + elseif corner and aplace[corner] then + ngeo = aplace[corner](mouse, {parent = c}) end - local x, y - if corner == "top_right" then - x = g.x + g.width - y = g.y - elseif corner == "top_left" then - x = g.x - y = g.y - elseif corner == "bottom_left" then - x = g.x - y = g.y + g.height - else - x = g.x + g.width - y = g.y + g.height - end - - return corner, x, y + return corner, ngeo and ngeo.x or nil, ngeo and ngeo.y or nil end --- Resize a client. +-- @function awful.mouse.client.resize -- @param c The client to resize, or the focused one by default. --- @param corner The corner to grab on resize. Auto detected by default. -function mouse.client.resize(c, corner) +-- @tparam string corner The corner to grab on resize. Auto detected by default. +-- @tparam[opt={}] table args A set of `awful.placement` arguments +-- @treturn string The corner (or side) name +function mouse.client.resize(c, corner, args) c = c or capi.client.focus if not c then return end @@ -357,19 +209,162 @@ function mouse.client.resize(c, corner) return end - local lay = layout.get(c.screen) - local corner2, x, y = mouse.client.corner(c, corner) + -- Move the mouse to the corner + if corner and aplace[corner] then + aplace[corner](capi.mouse, {parent=c}) + else + local _ + _, corner = aplace.closest_corner(capi.mouse, {parent=c}) + end - if lay == layout.suit.floating or c.floating then - return layout.suit.floating.mouse_resize_handler(c, corner2, x, y) - elseif lay.mouse_resize_handler then - return lay.mouse_resize_handler(c, corner2, x, y) + mouse.resize(c, "mouse.resize", args or {include_sides=true}) + + return corner +end + +--- Default handler for `request::geometry` signals with `mouse.resize` context. +-- @signalhandler awful.mouse.resize_handler +-- @tparam client c The client +-- @tparam string context The context +-- @tparam[opt={}] table hints The hints to pass to the handler +function mouse.resize_handler(c, context, hints) + if hints and context and context:find("mouse.*") then + -- This handler only handle the floating clients. If the client is tiled, + -- then it let the layouts handle it. + local lay = c.screen.selected_tag.layout + + if lay == layout.suit.floating or c.floating then + local offset = hints and hints.offset or {} + + if type(offset) == "number" then + offset = { + x = offset, + y = offset, + width = offset, + height = offset, + } + end + + c:geometry { + x = hints.x + (offset.x or 0 ), + y = hints.y + (offset.y or 0 ), + width = hints.width + (offset.width or 0 ), + height = hints.height + (offset.height or 0 ), + } + elseif lay.resize_handler then + lay.resize_handler(c, context, hints) + end end end +-- Older layouts implement their own mousegrabber. +-- @tparam client c The client +-- @tparam table args Additional arguments +-- @treturn boolean This return false when the resize need to be aborted +mouse.resize.add_enter_callback(function(c, args) --luacheck: no unused args + if c.floating then return end + + local l = c.screen.selected_tag and c.screen.selected_tag.layout or nil + if l == layout.suit.floating then return end + + if l ~= layout.suit.floating and l.mouse_resize_handler then + capi.mousegrabber.stop() + + local geo, corner = aplace.closest_corner(capi.mouse, {parent=c}) + + l.mouse_resize_handler(c, corner, geo.x, geo.y) + + return false + end +end, "mouse.resize") + +--- Get the client currently under the mouse cursor. +-- @property current_client +-- @tparam client|nil The client + +function mouse.object.get_current_client() + local obj = capi.mouse.object_under_pointer() + if type(obj) == "client" then + return obj + end +end + +function mouse.object.set_current_client() end + +--- Get the wibox currently under the mouse cursor. +-- @property current_wibox +-- @tparam wibox|nil The wibox + +function mouse.object.get_current_wibox() + local obj = capi.mouse.object_under_pointer() + if type(obj) == "drawin" then + return obj + end +end + +function mouse.object.set_current_wibox() end + +--- True if the left mouse button is pressed. +-- @property is_left_mouse_button_pressed +-- @param boolean + +--- True if the right mouse button is pressed. +-- @property is_right_mouse_button_pressed +-- @param boolean + +--- True if the middle mouse button is pressed. +-- @property is_middle_mouse_button_pressed +-- @param boolean + +for _, b in ipairs {"left", "right", "middle"} do + mouse.object["is_".. b .."_mouse_button_pressed"] = function() + return capi.mouse.coords().buttons[1] + end + + mouse.object["set_is_".. b .."_mouse_button_pressed"] = function() end +end + +capi.client.connect_signal("request::geometry", mouse.resize_handler) + -- Set the cursor at startup capi.root.cursor("left_ptr") +-- Implement the custom property handler +local props = {} + +capi.mouse.set_newindex_miss_handler(function(_,key,value) + if mouse.object["set_"..key] then + mouse.object["set_"..key](value) + else + props[key] = value + end +end) + +capi.mouse.set_index_miss_handler(function(_,key) + if mouse.object["get_"..key] then + return mouse.object["get_"..key]() + else + return props[key] + end +end) + +--- Get or set the mouse coords. +-- +--@DOC_awful_mouse_coords_EXAMPLE@ +-- +-- @tparam[opt=nil] table coords_table None or a table with x and y keys as mouse +-- coordinates. +-- @tparam[opt=nil] integer coords_table.x The mouse horizontal position +-- @tparam[opt=nil] integer coords_table.y The mouse vertical position +-- @tparam[opt=false] boolean silent Disable mouse::enter or mouse::leave events that +-- could be triggered by the pointer when moving. +-- @treturn integer table.x The horizontal position +-- @treturn integer table.y The vertical position +-- @treturn table table.buttons Table containing the status of buttons, e.g. field [1] is true +-- when button 1 is pressed. +-- @function mouse.coords + + return mouse -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/mouse/resize.lua b/lib/awful/mouse/resize.lua new file mode 100644 index 00000000..ed95fbbd --- /dev/null +++ b/lib/awful/mouse/resize.lua @@ -0,0 +1,199 @@ +--------------------------------------------------------------------------- +--- An extandable mouse resizing handler. +-- +-- This module offer a resizing and moving mechanism for drawable such as +-- clients and wiboxes. +-- +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2016 Emmanuel Lepage Vallee +-- @release @AWESOME_VERSION@ +-- @submodule mouse +--------------------------------------------------------------------------- + +local aplace = require("awful.placement") +local capi = {mousegrabber = mousegrabber} + +local module = {} + +local mode = "live" +local req = "request::geometry" +local callbacks = {enter={}, move={}, leave={}} + +--- Set the resize mode. +-- The available modes are: +-- +-- * **live**: Resize the layout everytime the mouse move +-- * **after**: Resize the layout only when the mouse is released +-- +-- Some clients, such as XTerm, may lose information if resized too often. +-- +-- @function awful.mouse.resize.set_mode +-- @tparam string m The mode +function module.set_mode(m) + assert(m == "live" or m == "after") + mode = m +end + +--- Add a initialization callback. +-- This callback will be executed before the mouse grabbing start +-- @function awful.mouse.resize.add_enter_callback +-- @tparam function cb The callback (or nil) +-- @tparam[default=other] string context The callback context +function module.add_enter_callback(cb, context) + context = context or "other" + callbacks.enter[context] = callbacks.enter[context] or {} + table.insert(callbacks.enter[context], cb) +end + +--- Add a "move" callback. +-- This callback is executed in "after" mode (see `set_mode`) instead of +-- applying the operation. +-- @function awful.mouse.resize.add_move_callback +-- @tparam function cb The callback (or nil) +-- @tparam[default=other] string context The callback context +function module.add_move_callback(cb, context) + context = context or "other" + callbacks.move[context] = callbacks.move[context] or {} + table.insert(callbacks.move[context], cb) +end + +--- Add a "leave" callback +-- This callback is executed just before the `mousegrabber` stop +-- @function awful.mouse.resize.add_leave_callback +-- @tparam function cb The callback (or nil) +-- @tparam[default=other] string context The callback context +function module.add_leave_callback(cb, context) + context = context or "other" + callbacks.leave[context] = callbacks.leave[context] or {} + table.insert(callbacks.leave[context], cb) +end + +-- Resize, the drawable. +-- +-- Valid `args` are: +-- +-- * *enter_callback*: A function called before the `mousegrabber` start +-- * *move_callback*: A function called when the mouse move +-- * *leave_callback*: A function called before the `mousegrabber` is released +-- * *mode*: The resize mode +-- +-- @function awful.mouse.resize +-- @tparam client client A client +-- @tparam[default=mouse.resize] string context The resizing context +-- @tparam[opt={}] table args A set of `awful.placement` arguments + +local function handler(_, client, context, args) --luacheck: no unused_args + args = args or {} + context = context or "mouse.resize" + + local placement = args.placement + + if type(placement) == "string" and aplace[placement] then + placement = aplace[placement] + end + + -- Extend the table with the default arguments + args = setmetatable( + { + placement = placement or aplace.resize_to_mouse, + mode = args.mode or mode, + pretend = true, + }, + {__index = args or {}} + ) + + local geo + + for _, cb in ipairs(callbacks.enter[context] or {}) do + geo = cb(client, args) + + if geo == false then + return false + end + end + + if args.enter_callback then + geo = args.enter_callback(client, args) + + if geo == false then + return false + end + end + + geo = nil + + -- Execute the placement function and use request::geometry + capi.mousegrabber.run(function (_mouse) + if not client.valid then return end + + -- Resize everytime the mouse move (default behavior) + if args.mode == "live" then + -- Get the new geometry + geo = setmetatable(args.placement(client, args),{__index=args}) + end + + -- Execute the move callbacks. This can be used to add features such as + -- snap or adding fancy graphical effects. + for _, cb in ipairs(callbacks.move[context] or {}) do + -- If something is returned, assume it is a modified geometry + geo = cb(client, geo, args) or geo + + if geo == false then + return false + end + end + + if args.move_callback then + geo = args.move_callback(client, geo, args) + + if geo == false then + return false + end + end + + -- In case it was modified + setmetatable(geo,{__index=args}) + + if args.mode == "live" then + -- Ask the resizing handler to resize the client + client:emit_signal( req, context, geo) + end + + -- Quit when the button is released + for _,v in pairs(_mouse.buttons) do + if v then return true end + end + + -- Only resize after the mouse is released, this avoid losing content + -- in resize sensitive apps such as XTerm or allow external modules + -- to implement custom resizing + if args.mode == "after" then + -- Get the new geometry + geo = args.placement(client, args) + + -- Ask the resizing handler to resize the client + client:emit_signal( req, context, geo) + end + + geo = nil + + for _, cb in ipairs(callbacks.leave[context] or {}) do + geo = cb(client, geo, args) + end + + if args.leave_callback then + geo = args.leave_callback(client, geo, args) + end + + if not geo then return false end + + -- In case it was modified + setmetatable(geo,{__index=args}) + + client:emit_signal( req, context, geo) + + return false + end, "cross") +end + +return setmetatable(module, {__call=handler}) diff --git a/lib/awful/mouse/snap.lua b/lib/awful/mouse/snap.lua new file mode 100644 index 00000000..684217cd --- /dev/null +++ b/lib/awful/mouse/snap.lua @@ -0,0 +1,269 @@ +--------------------------------------------------------------------------- +--- Mouse snapping related functions +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @release @AWESOME_VERSION@ +-- @submodule mouse +--------------------------------------------------------------------------- + +local aclient = require("awful.client") +local resize = require("awful.mouse.resize") +local aplace = require("awful.placement") +local wibox = require("wibox") +local beautiful = require("beautiful") +local color = require("gears.color") +local shape = require("gears.shape") +local cairo = require("lgi").cairo + +local capi = { + root = root, + mouse = mouse, + screen = screen, + client = client, + mousegrabber = mousegrabber, +} + +local module = { + default_distance = 8 +} + +local placeholder_w = nil + +local function show_placeholder(geo) + if not geo then + if placeholder_w then + placeholder_w.visible = false + end + return + end + + placeholder_w = placeholder_w or wibox { + ontop = true, + bg = color(beautiful.snap_bg or beautiful.bg_urgent or "#ff0000"), + } + + placeholder_w:geometry(geo) + + local img = cairo.ImageSurface(cairo.Format.A1, geo.width, geo.height) + local cr = cairo.Context(img) + + cr:set_operator(cairo.Operator.CLEAR) + cr:set_source_rgba(0,0,0,1) + cr:paint() + cr:set_operator(cairo.Operator.SOURCE) + cr:set_source_rgba(1,1,1,1) + + local line_width = beautiful.snap_border_width or 5 + cr:set_line_width(beautiful.xresources.apply_dpi(line_width)) + + local f = beautiful.snap_shape or function() + cr:translate(line_width,line_width) + shape.rounded_rect(cr,geo.width-2*line_width,geo.height-2*line_width, 10) + end + + f(cr, geo.width, geo.height) + + cr:stroke() + + placeholder_w.shape_bounding = img._native + + placeholder_w.visible = true +end + +local function build_placement(snap, axis) + return aplace.scale + + aplace[snap] + + ( + axis and aplace["maximize_"..axis] or nil + ) +end + +local function detect_screen_edges(c, snap) + local coords = capi.mouse.coords() + + local sg = c.screen.geometry + + local v, h = nil + + if math.abs(coords.x) <= snap + sg.x and coords.x >= sg.x then + h = "left" + elseif math.abs((sg.x + sg.width) - coords.x) <= snap then + h = "right" + end + + if math.abs(coords.y) <= snap + sg.y and coords.y >= sg.y then + v = "top" + elseif math.abs((sg.y + sg.height) - coords.y) <= snap then + v = "bottom" + end + + return v, h +end + +local current_snap, current_axis = nil + +local function detect_areasnap(c, distance) + local old_snap = current_snap + local v, h = detect_screen_edges(c, distance) + + if v and h then + current_snap = v.."_"..h + else + current_snap = v or h or nil + end + + if old_snap == current_snap then return end + + current_axis = ((v and not h) and "horizontally") + or ((h and not v) and "vertically") + or nil + + -- Show the expected geometry outline + show_placeholder( + current_snap and build_placement(current_snap, current_axis)(c, { + to_percent = 0.5, + honor_workarea = true, + pretend = true + }) or nil + ) + +end + +local function apply_areasnap(c, args) + if not current_snap then return end + + -- Remove the move offset + args.offset = {} + + placeholder_w.visible = false + + return build_placement(current_snap, current_axis)(c,{ + to_percent = 0.5, + honor_workarea = true, + }) +end + +local function snap_outside(g, sg, snap) + if g.x < snap + sg.x + sg.width and g.x > sg.x + sg.width then + g.x = sg.x + sg.width + elseif g.x + g.width < sg.x and g.x + g.width > sg.x - snap then + g.x = sg.x - g.width + end + if g.y < snap + sg.y + sg.height and g.y > sg.y + sg.height then + g.y = sg.y + sg.height + elseif g.y + g.height < sg.y and g.y + g.height > sg.y - snap then + g.y = sg.y - g.height + end + return g +end + +local function snap_inside(g, sg, snap) + local edgev = 'none' + local edgeh = 'none' + if math.abs(g.x) < snap + sg.x and g.x > sg.x then + edgev = 'left' + g.x = sg.x + elseif math.abs((sg.x + sg.width) - (g.x + g.width)) < snap then + edgev = 'right' + g.x = sg.x + sg.width - g.width + end + if math.abs(g.y) < snap + sg.y and g.y > sg.y then + edgeh = 'top' + g.y = sg.y + elseif math.abs((sg.y + sg.height) - (g.y + g.height)) < snap then + edgeh = 'bottom' + g.y = sg.y + sg.height - g.height + end + + -- What is the dominant dimension? + if g.width > g.height then + return g, edgeh + else + return g, edgev + end +end + +--- Snap a client to the closest client or screen edge. +-- @function awful.mouse.snap +-- @param c The client to snap. +-- @param snap The pixel to snap clients. +-- @param x The client x coordinate. +-- @param y The client y coordinate. +-- @param fixed_x True if the client isn't allowed to move in the x direction. +-- @param fixed_y True if the client isn't allowed to move in the y direction. +function module.snap(c, snap, x, y, fixed_x, fixed_y) + snap = snap or module.default_distance + c = c or capi.client.focus + local cur_geom = c:geometry() + local geom = c:geometry() + geom.width = geom.width + (2 * c.border_width) + geom.height = geom.height + (2 * c.border_width) + local edge + geom.x = x or geom.x + geom.y = y or geom.y + + geom, edge = snap_inside(geom, capi.screen[c.screen].geometry, snap) + geom = snap_inside(geom, capi.screen[c.screen].workarea, snap) + + -- Allow certain windows to snap to the edge of the workarea. + -- Only allow docking to workarea for consistency/to avoid problems. + if c.dockable then + local struts = c:struts() + struts['left'] = 0 + struts['right'] = 0 + struts['top'] = 0 + struts['bottom'] = 0 + if edge ~= "none" and c.floating then + if edge == "left" or edge == "right" then + struts[edge] = cur_geom.width + elseif edge == "top" or edge == "bottom" then + struts[edge] = cur_geom.height + end + end + c:struts(struts) + end + + geom.x = geom.x - (2 * c.border_width) + geom.y = geom.y - (2 * c.border_width) + + for _, snapper in ipairs(aclient.visible(c.screen)) do + if snapper ~= c then + geom = snap_outside(geom, snapper:geometry(), snap) + end + end + + geom.width = geom.width - (2 * c.border_width) + geom.height = geom.height - (2 * c.border_width) + geom.x = geom.x + (2 * c.border_width) + geom.y = geom.y + (2 * c.border_width) + + -- It's easiest to undo changes afterwards if they're not allowed + if fixed_x then geom.x = cur_geom.x end + if fixed_y then geom.y = cur_geom.y end + + return geom +end + +-- Enable edge snapping +resize.add_move_callback(function(c, geo, args) + -- Screen edge snapping (areosnap) + if (module.edge_enabled ~= false) + and args and (args.snap == nil or args.snap) then + detect_areasnap(c, 16) + end + + -- Snapping between clients + if (module.client_enabled ~= false) + and args and (args.snap == nil or args.snap) then + return module.snap(c, args.snap, geo.x, geo.y) + end +end, "mouse.move") + +-- Apply the aerosnap +resize.add_leave_callback(function(c, _, args) + if module.edge_enabled == false then return end + return apply_areasnap(c, args) +end, "mouse.move") + +return setmetatable(module, {__call = function(_, ...) return module.snap(...) end}) diff --git a/lib/awful/placement.lua b/lib/awful/placement.lua index b98b2a67..1f2dad32 100644 --- a/lib/awful/placement.lua +++ b/lib/awful/placement.lua @@ -9,14 +9,21 @@ -- * Turn each function into an API with various common customization parameters. -- * Re-use the same functions for the `mouse`, `client`s, `screen`s and `wibox`es -- --- +-- +--

Compositing

-- -- It is possible to compose placement function using the `+` or `*` operator: -- --- local f = (awful.placement.right + awful.placement.left) --- f(client.focus) +--@DOC_awful_placement_compose_EXAMPLE@ -- --- ### Common arguments +--@DOC_awful_placement_compose2_EXAMPLE@ +-- +--

Common arguments

+-- +-- **pretend** (*boolean*): +-- +-- Do not apply the new geometry. This is useful if only the return values is +-- necessary. -- -- **honor_workarea** (*boolean*): -- @@ -44,6 +51,10 @@ -- -- **attach** (*boolean*): -- +-- **offset** (*table or number*): +-- +-- The offset(s) to apply to the new geometry. +-- -- **store_geometry** (*boolean*): -- -- Keep a single history of each type of placement. It can be restored using @@ -77,6 +88,7 @@ local capi = local client = require("awful.client") local layout = require("awful.layout") local a_screen = require("awful.screen") +local util = require("awful.util") local dpi = require("beautiful").xresources.apply_dpi local function get_screen(s) @@ -85,20 +97,89 @@ end local wrap_client = nil -local function compose(w1, w2) - return wrap_client(function(...) - w1(...) - w2(...) - return --It make no sense to keep a return value - end) +--- Allow multiple placement functions to be daisy chained. +-- This also allow the functions to be aware they are being chained and act +-- upon the previous nodes results to avoid unnecessary processing or deduce +-- extra paramaters/arguments. +local function compose(...) + local queue = {} + + local nodes = {...} + + -- Allow placement.foo + (var == 42 and placement.bar) + if not nodes[2] then + return nodes[1] + end + + -- nodes[1] == self, nodes[2] == other + for _, w in ipairs(nodes) do + -- Build an execution queue + if w.context and w.context == "compose" then + for _, elem in ipairs(w.queue or {}) do + table.insert(queue, elem) + end + else + table.insert(queue, w) + end + end + + local ret = wrap_client(function(d, args, ...) + local rets = {} + local last_geo = nil + + -- As some functions may have to take into account results from + -- previously execued ones, add the `composition_results` hint. + args = setmetatable({composition_results=rets}, {__index=args}) + + -- Only apply the geometry once, not once per chain node, to do this, + -- Force the "pretend" argument and restore the original value for + -- the last node. + local pretend_real = args.pretend + + args.pretend = true + + for k, f in ipairs(queue) do + if k == #queue then + args.pretend = pretend_real or false + end + + local r = {f(d, args, ...)} + last_geo = r[1] or last_geo + args.override_geometry = last_geo + + -- Keep the return value, store one per context + if f.context then + -- When 2 composition queue are executed, merge the return values + if f.context == "compose" then + for k2,v in pairs(r) do + rets[k2] = v + end + else + rets[f.context] = r + end + end + end + + return last_geo, rets + end, "compose") + + ret.queue = queue + + return ret end -wrap_client = function(f) - return setmetatable({is_placement=true}, { - __call = function(_,...) return f(...) end, - __add = compose, -- Composition is usually defined as + - __mul = compose -- Make sense if you think of the functions as matrices - }) +wrap_client = function(f, context) + return setmetatable( + { + is_placement= true, + context = context, + }, + { + __call = function(_,...) return f(...) end, + __add = compose, -- Composition is usually defined as + + __mul = compose -- Make sense if you think of the functions as matrices + } + ) end local placement_private = {} @@ -111,7 +192,7 @@ local placement_private = {} local placement = setmetatable({}, { __index = placement_private, __newindex = function(_, k, f) - placement_private[k] = wrap_client(f) + placement_private[k] = wrap_client(f, k) end }) @@ -143,6 +224,21 @@ local align_map = { -- Store function -> keys local reverse_align_map = {} +-- Some parameters to correctly compute the final size +local resize_to_point_map = { + -- Corners + top_left = {p1= nil , p2={1,1}, x_only=false, y_only=false, align="bottom_right"}, + top_right = {p1={0,1} , p2= nil , x_only=false, y_only=false, align="bottom_left" }, + bottom_left = {p1= nil , p2={1,0}, x_only=false, y_only=false, align="top_right" }, + bottom_right = {p1={0,0} , p2= nil , x_only=false, y_only=false, align="top_left" }, + + -- Sides + left = {p1= nil , p2={1,1}, x_only=true , y_only=false, align="top_right" }, + right = {p1={0,0} , p2= nil , x_only=true , y_only=false, align="top_left" }, + top = {p1= nil , p2={1,1}, x_only=false, y_only=true , align="bottom_left" }, + bottom = {p1={0,0} , p2= nil , x_only=false, y_only=true , align="top_left" }, +} + --- Add a context to the arguments. -- This function extend the argument table. The context is used by some -- internal helper methods. If there already is a context, it has priority and @@ -163,17 +259,49 @@ local function store_geometry(d, reqtype) data[d][reqtype].screen = d.screen end +--- Apply some modifications before applying the new geometry. +-- @tparam table new_geo The new geometry +-- @tparam table args The common arguments +-- @treturn table|nil The new geometry +local function fix_new_geometry(new_geo, args) + if args.pretend or not new_geo then return nil end + + local offset = args.offset or {} + + if type(offset) == "number" then + offset = { + x = offset, + y = offset, + width = offset, + height = offset, + } + end + + return { + x = new_geo.x and (new_geo.x + (offset.x or 0)), + y = new_geo.y and (new_geo.y + (offset.y or 0)), + width = new_geo.width and (new_geo.width + (offset.width or 0)), + height = new_geo.height and (new_geo.height + (offset.height or 0)), + } +end + --- Get the area covered by a drawin. -- @param d The drawin -- @tparam[opt=nil] table new_geo A new geometry -- @tparam[opt=false] boolean ignore_border_width Ignore the border +-- @tparam table args the method arguments -- @treturn The drawin's area. -local function area_common(d, new_geo, ignore_border_width) +local function area_common(d, new_geo, ignore_border_width, args) -- The C side expect no arguments, nil isn't valid local geometry = new_geo and d:geometry(new_geo) or d:geometry() local border = ignore_border_width and 0 or d.border_width or 0 - geometry.x = geometry.x - geometry.y = geometry.y + + -- When using the placement composition along with the "pretend" + -- option, it is necessary to keep a "virtual" geometry. + if args and args.override_geometry then + geometry = util.table.clone(args.override_geometry) + end + geometry.width = geometry.width + 2 * border geometry.height = geometry.height + 2 * border return geometry @@ -196,14 +324,17 @@ local function geometry_common(obj, args, new_geo, ignore_border_width) -- It's a mouse if obj.coords then - local coords = new_geo and obj.coords(new_geo) or obj.coords() + local coords = fix_new_geometry(new_geo, args) + and obj.coords(new_geo) or obj.coords() return {x=coords.x, y=coords.y, width=0, height=0} elseif obj.geometry then local geo = obj.geometry -- It is either a drawable or something that implement its API if type(geo) == "function" then - local dgeo = area_common(obj, new_geo, ignore_border_width) + local dgeo = area_common( + obj, fix_new_geometry(new_geo, args), ignore_border_width, args + ) -- Apply the margins if args.margins then @@ -434,6 +565,24 @@ local function area_remove(areas, elem) return areas end +-- Convert 2 points into a rectangle +local function rect_from_points(p1x, p1y, p2x, p2y) + return { + x = p1x, + y = p1y, + width = p2x - p1x, + height = p2y - p1y, + } +end + +-- Convert a rectangle and matrix info into a point +local function rect_to_point(rect, corner_i, corner_j) + return { + x = rect.x + corner_i * math.floor(rect.width ), + y = rect.y + corner_j * math.floor(rect.height), + } +end + --- Move a drawable to the closest corner of the parent geometry (such as the -- screen). -- @@ -445,6 +594,7 @@ end -- @tparam[opt=client.focus] drawable d A drawable (like `client`, `mouse` -- or `wibox`) -- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry -- @treturn string The corner name function placement.closest_corner(d, args) args = add_context(args, "closest_corner") @@ -460,8 +610,10 @@ function placement.closest_corner(d, args) -- Use the product of 3 to get the closest point in a NxN matrix local function f(_n, mat) n = _n - corner_i = -math.ceil( ( (sgeo.x - pos.x) * n) / sgeo.width ) - corner_j = -math.ceil( ( (sgeo.y - pos.y) * n) / sgeo.height ) + -- The +1 is required to avoid a rounding error when + -- pos.x == sgeo.x+sgeo.width + corner_i = -math.ceil( ( (sgeo.x - pos.x) * n) / (sgeo.width + 1)) + corner_j = -math.ceil( ( (sgeo.y - pos.y) * n) / (sgeo.height + 1)) return mat[corner_j + 1][corner_i + 1] end @@ -476,9 +628,9 @@ function placement.closest_corner(d, args) -- Transpose the corner back to the original size local new_args = setmetatable({position = corner}, {__index=args}) - placement_private.align(d, new_args) + local ngeo = placement_private.align(d, new_args) - return corner + return ngeo, corner end --- Place the client so no part of it will be outside the screen (workarea). @@ -520,6 +672,7 @@ end --- Place the client where there's place available with minimum overlap. --@DOC_awful_placement_no_overlap_EXAMPLE@ -- @param c The client. +-- @treturn table The new geometry function placement.no_overlap(c) c = c or capi.client.focus local geometry = area_common(c) @@ -578,14 +731,24 @@ end --- Place the client under the mouse. --@DOC_awful_placement_under_mouse_EXAMPLE@ --- @param c The client. --- @return The new client geometry. -function placement.under_mouse(c) - c = c or capi.client.focus - local c_geometry = area_common(c) +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +function placement.under_mouse(d, args) + args = add_context(args, "under_mouse") + d = d or capi.client.focus + local m_coords = capi.mouse.coords() - return c:geometry({ x = m_coords.x - c_geometry.width / 2, - y = m_coords.y - c_geometry.height / 2 }) + + local ngeo = geometry_common(d, args) + ngeo.x = m_coords.x - ngeo.width / 2 + ngeo.y = m_coords.y - ngeo.height / 2 + + local bw = d.border_width or 0 + ngeo.width = ngeo.width - 2*bw + ngeo.height = ngeo.height - 2*bw + + return ngeo end --- Place the client next to the mouse. @@ -595,7 +758,7 @@ end --@DOC_awful_placement_next_to_mouse_EXAMPLE@ -- @client[opt=focused] c The client. -- @tparam[opt=apply_dpi(5)] integer offset The offset from the mouse position. --- @return The new client geometry. +-- @treturn table The new geometry function placement.next_to_mouse(c, offset) c = c or capi.client.focus offset = offset or dpi(5) @@ -627,6 +790,78 @@ function placement.next_to_mouse(c, offset) return c:geometry({ x = x, y = y }) end +--- Resize the drawable to the cursor. +-- +-- Valid args: +-- +-- * *axis*: The axis (vertical or horizontal). If none is +-- specified, then the drawable will be resized on both axis. +-- +--@DOC_awful_placement_resize_to_mouse_EXAMPLE@ +-- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) +-- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry +function placement.resize_to_mouse(d, args) + d = d or capi.client.focus + args = add_context(args, "resize_to_mouse") + + local coords = capi.mouse.coords() + local ngeo = geometry_common(d, args) + local h_only = args.axis == "horizontal" + local v_only = args.axis == "vertical" + + -- To support both growing and shrinking the drawable, it is necessary + -- to decide to use either "north or south" and "east or west" directions. + -- Otherwise, the result will always be 1x1 + local _, closest_corner = placement.closest_corner(capi.mouse, { + parent = d, + pretend = true, + include_sides = args.include_sides or false, + }) + + -- Given "include_sides" wasn't set, it will always return a name + -- with the 2 axis. If only one axis is needed, adjust the result + if h_only then + closest_corner = closest_corner:match("left") or closest_corner:match("right") + elseif v_only then + closest_corner = closest_corner:match("top") or closest_corner:match("bottom") + end + + -- Use p0 (mouse), p1 and p2 to create a rectangle + local pts = resize_to_point_map[closest_corner] + local p1 = pts.p1 and rect_to_point(ngeo, pts.p1[1], pts.p1[2]) or coords + local p2 = pts.p2 and rect_to_point(ngeo, pts.p2[1], pts.p2[2]) or coords + + -- Create top_left and bottom_right points, convert to rectangle + ngeo = rect_from_points( + pts.y_only and ngeo.x or math.min(p1.x, p2.x), + pts.x_only and ngeo.y or math.min(p1.y, p2.y), + pts.y_only and ngeo.x + ngeo.width or math.max(p2.x, p1.x), + pts.x_only and ngeo.y + ngeo.height or math.max(p2.y, p1.y) + ) + + local bw = d.border_width or 0 + + for _, a in ipairs {"width", "height"} do + ngeo[a] = ngeo[a] - 2*bw + end + + -- Now, correct the geometry by the given size_hints offset + if d.apply_size_hints then + local w, h = d:apply_size_hints( + ngeo.width, + ngeo.height + ) + local offset = align_map[pts.align](w, h, ngeo.width, ngeo.height) + ngeo.x = ngeo.x - offset.x + ngeo.y = ngeo.y - offset.y + end + + geometry_common(d, args, ngeo) + + return ngeo +end + --- Move the drawable (client or wibox) `d` to a screen position or side. -- -- Supported args.positions are: @@ -646,6 +881,7 @@ end --@DOC_awful_placement_align_EXAMPLE@ -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) -- @tparam[opt={}] table args Other arguments +-- @treturn table The new geometry function placement.align(d, args) args = add_context(args, "align") d = d or capi.client.focus @@ -663,14 +899,18 @@ function placement.align(d, args) dgeo.height ) - geometry_common(d, args, { + local ngeo = { x = (pos.x and math.ceil(sgeo.x + pos.x) or dgeo.x) , y = (pos.y and math.ceil(sgeo.y + pos.y) or dgeo.y) , width = math.ceil(dgeo.width ) - 2*bw, height = math.ceil(dgeo.height ) - 2*bw, - }) + } + + geometry_common(d, args, ngeo) attach(d, placement[args.position], args) + + return ngeo end -- Add the alias functions @@ -678,7 +918,7 @@ for k in pairs(align_map) do placement[k] = function(d, args) args = add_context(args, k) args.position = k - placement_private.align(d, args) + return placement_private.align(d, args) end reverse_align_map[placement[k]] = k end @@ -716,6 +956,7 @@ end --@DOC_awful_placement_stretch_EXAMPLE@ -- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`) -- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry function placement.stretch(d, args) args = add_context(args, "stretch") @@ -757,6 +998,8 @@ function placement.stretch(d, args) geometry_common(d, args, ngeo) attach(d, placement["stretch_"..args.direction], args) + + return ngeo end -- Add the alias functions @@ -764,7 +1007,7 @@ for _,v in ipairs {"left", "right", "up", "down"} do placement["stretch_"..v] = function(d, args) args = add_context(args, "stretch_"..v) args.direction = v - placement_private.stretch(d, args) + return placement_private.stretch(d, args) end end @@ -785,6 +1028,7 @@ end --@DOC_awful_placement_maximize_EXAMPLE@ -- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`) -- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry function placement.maximize(d, args) args = add_context(args, "maximize") d = d or capi.client.focus @@ -808,6 +1052,8 @@ function placement.maximize(d, args) geometry_common(d, args, ngeo) attach(d, placement.maximize, args) + + return ngeo end -- Add the alias functions @@ -815,10 +1061,66 @@ for _, v in ipairs {"vertically", "horizontally"} do placement["maximize_"..v] = function(d2, args) args = add_context(args, "maximize_"..v) args.axis = v - placement_private.maximize(d2, args) + return placement_private.maximize(d2, args) end end +--- Scale the drawable by either a relative or absolute percent. +-- +-- Valid args: +-- +-- **to_percent** : A number between 0 and 1. It represent a percent related to +-- the parent geometry. +-- **by_percent** : A number between 0 and 1. It represent a percent related to +-- the current size. +-- **direction**: Nothing or "left", "right", "up", "down". +-- +-- @tparam[opt=client.focus] drawable d A drawable (like `client` or `wibox`) +-- @tparam[opt={}] table args The arguments +-- @treturn table The new geometry +function placement.scale(d, args) + args = add_context(args, "scale_to_percent") + d = d or capi.client.focus + + local to_percent = args.to_percent + local by_percent = args.by_percent + + local percent = to_percent or by_percent + + local direction = args.direction + + local sgeo = get_parent_geometry(d, args) + local ngeo = geometry_common(d, args, nil) + + local old_area = {width = ngeo.width, height = ngeo.height} + + if (not direction) or direction == "left" or direction == "right" then + ngeo.width = (to_percent and sgeo or ngeo).width*percent + + if direction == "left" then + ngeo.x = ngeo.x - (ngeo.width - old_area.width) + end + end + + if (not direction) or direction == "up" or direction == "down" then + ngeo.height = (to_percent and sgeo or ngeo).height*percent + + if direction == "up" then + ngeo.y = ngeo.y - (ngeo.height - old_area.height) + end + end + + local bw = d.border_width or 0 + ngeo.width = ngeo.width - 2*bw + ngeo.height = ngeo.height - 2*bw + + geometry_common(d, args, ngeo) + + attach(d, placement.maximize, args) + + return ngeo +end + ---@DOC_awful_placement_maximize_vertically_EXAMPLE@ ---@DOC_awful_placement_maximize_horizontally_EXAMPLE@ diff --git a/mouse.c b/mouse.c index 5c80b17b..272b12ad 100644 --- a/mouse.c +++ b/mouse.c @@ -19,7 +19,39 @@ * */ -/** awesome mouse API +/** awesome mouse API. + * + * The mouse buttons are represented as index. The common ones are: + * + * ![Client geometry](../images/mouse.svg) + * + * It is possible to be notified of mouse events by connecting to various + * `client`, `widget`s and `wibox` signals: + * + * * `mouse::enter` + * * `mouse::leave` + * * `mouse::press` + * * `mouse::release` + * * `mouse::move` + * + * It is also possible to add generic mouse button callbacks for `client`s, + * `wiboxe`s and the `root` window. Those are set in the default `rc.lua` as such: + * + * **root**: + * + * root.buttons(awful.util.table.join( + * awful.button({ }, 3, function () mymainmenu:toggle() end), + * awful.button({ }, 4, awful.tag.viewnext), + * awful.button({ }, 5, awful.tag.viewprev) + * )) + * + * **client**: + * + * clientbuttons = awful.util.table.join( + * awful.button({ }, 1, function (c) client.focus = c; c:raise() end), + * awful.button({ modkey }, 1, awful.mouse.client.move), + * awful.button({ modkey }, 3, awful.mouse.client.resize) + * ) * * See also `mousegrabber` * @@ -33,27 +65,19 @@ #include "math.h" #include "common/util.h" #include "common/xutil.h" +#include "common/luaclass.h" #include "globalconf.h" #include "objects/client.h" #include "objects/drawin.h" #include "objects/screen.h" -/** Mouse library. - * - * @table mouse - */ +static int miss_index_handler = LUA_REFNIL; +static int miss_newindex_handler = LUA_REFNIL; /** * The `screen` under the cursor - * @field screen - */ - -/** A table with X and Y coordinates. - * @field x X coordinate. - * @field y Y coordinate. - * @field buttons Table containing the status of buttons, e.g. field [1] is true - * when button 1 is pressed. - * @table coords_table + * @property screen + * @param screen */ /** Get the pointer position. @@ -119,6 +143,39 @@ mouse_warp_pointer(xcb_window_t window, int16_t x, int16_t y) 0, 0, 0, 0, x, y); } +/** + * Allow the a Lua handler to be implemented for custom properties and + * functions. + * \param L A lua state + * \param handler A function on the LUA_REGISTRYINDEX + */ +static int +luaA_mouse_call_handler(lua_State *L, int handler) +{ + int nargs = lua_gettop(L); + + /* Push error handling function and move it before args */ + lua_pushcfunction(L, luaA_dofunction_error); + lua_insert(L, - nargs - 1); + int error_func_pos = 1; + + /* push function and move it before args */ + lua_rawgeti(L, LUA_REGISTRYINDEX, handler); + lua_insert(L, - nargs - 1); + + if(lua_pcall(L, nargs, LUA_MULTRET, error_func_pos)) + { + warn("%s", lua_tostring(L, -1)); + /* Remove error function and error string */ + lua_pop(L, 2); + return 0; + } + /* Remove error function */ + lua_remove(L, error_func_pos); + + return lua_gettop(L); +} + /** Mouse library. * \param L The Lua VM state. * \return The number of elements pushed on stack. @@ -133,8 +190,13 @@ luaA_mouse_index(lua_State *L) int16_t mouse_x, mouse_y; /* attr is not "screen"?! */ - if (A_STRNEQ(attr, "screen")) - return luaA_default_index(L); + if (A_STRNEQ(attr, "screen")) { + if (miss_index_handler != LUA_REFNIL) { + return luaA_mouse_call_handler(L, miss_index_handler); + } + else + return luaA_default_index(L); + } if (!mouse_query_pointer_root(&mouse_x, &mouse_y, NULL, NULL)) { @@ -162,8 +224,14 @@ luaA_mouse_newindex(lua_State *L) const char *attr = luaL_checkstring(L, 2); screen_t *screen; - if (A_STRNEQ(attr, "screen")) - return luaA_default_newindex(L); + if (A_STRNEQ(attr, "screen")) { + /* Call the lua mouse property handler */ + if (miss_newindex_handler != LUA_REFNIL) { + return luaA_mouse_call_handler(L, miss_newindex_handler); + } + else + return luaA_default_newindex(L); + } screen = luaA_checkscreen(L, 3); mouse_warp_pointer(globalconf.screen->root, screen->geometry.x, screen->geometry.y); @@ -201,15 +269,7 @@ luaA_mouse_pushstatus(lua_State *L, int x, int y, uint16_t mask) return 1; } -/** Get or set the mouse coords. - * - * @tparam coords_table coords_table None or a table with x and y keys as mouse - * coordinates. - * @tparam boolean silent Disable mouse::enter or mouse::leave events that - * could be triggered by the pointer when moving. - * @treturn coords_table A table with mouse coordinates. - * @function coords - */ +/* documented in lib/awful/mouse/init.lua */ static int luaA_mouse_coords(lua_State *L) { @@ -271,12 +331,32 @@ luaA_mouse_object_under_pointer(lua_State *L) return 0; } +/** + * Add a custom property handler (getter). + */ +static int +luaA_mouse_set_index_miss_handler(lua_State *L) +{ + return luaA_registerfct(L, 1, &miss_index_handler); +} + +/** + * Add a custom property handler (setter). + */ +static int +luaA_mouse_set_newindex_miss_handler(lua_State *L) +{ + return luaA_registerfct(L, 1, &miss_newindex_handler); +} + const struct luaL_Reg awesome_mouse_methods[] = { { "__index", luaA_mouse_index }, { "__newindex", luaA_mouse_newindex }, { "coords", luaA_mouse_coords }, { "object_under_pointer", luaA_mouse_object_under_pointer }, + { "set_index_miss_handler", luaA_mouse_set_index_miss_handler}, + { "set_newindex_miss_handler", luaA_mouse_set_newindex_miss_handler}, { NULL, NULL } }; const struct luaL_Reg awesome_mouse_meta[] = diff --git a/tests/examples/CMakeLists.txt b/tests/examples/CMakeLists.txt index 71469153..0c40ede3 100644 --- a/tests/examples/CMakeLists.txt +++ b/tests/examples/CMakeLists.txt @@ -46,7 +46,6 @@ function(escape_string variable content escaped_content line_prefix) if(variable MATCHES "--DOC_HIDE_ALL") return() endif() - string(REGEX REPLACE "\n" ";" var_lines "${variable}") set(tmp_output ${content}) @@ -201,10 +200,17 @@ function(run_test test_path namespace template escaped_content) # Only add it if there is something to display. if(NOT ${TEST_CODE} STREQUAL "\n--") + # Do not use the @usage tag, use 4 spaces + file(READ ${test_path} tmp_content) + if(NOT tmp_content MATCHES "--DOC_NO_USAGE") + set(DOC_PREFIX "@usage") + endif() + escape_string( - " @usage" + " ${DOC_PREFIX}" "${TEST_DOC_CONTENT}" TEST_DOC_CONTENT "" ) + set(TEST_DOC_CONTENT "${TEST_DOC_CONTENT}${TEST_CODE}") endif() diff --git a/tests/examples/awful/mouse/coords.lua b/tests/examples/awful/mouse/coords.lua new file mode 100644 index 00000000..b4b0ed95 --- /dev/null +++ b/tests/examples/awful/mouse/coords.lua @@ -0,0 +1,13 @@ +screen[1]._resize {x = 175, width = 128, height = 96} --DOC_HIDE +mouse.coords {x=175+60,y=60} --DOC_HIDE + + -- Get the position +print(mouse.coords().x) + + -- Change the position +mouse.coords { + x = 185, + y = 10 +} + +mouse.push_history() --DOC_HIDE diff --git a/tests/examples/awful/placement/bottom.lua b/tests/examples/awful/placement/bottom.lua index 10dff9ab..6af09262 100644 --- a/tests/examples/awful/placement/bottom.lua +++ b/tests/examples/awful/placement/bottom.lua @@ -1,6 +1,7 @@ -- Align a client to the bottom of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name bottom --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/bottom_left.lua b/tests/examples/awful/placement/bottom_left.lua index a9ebc062..4d4cff37 100644 --- a/tests/examples/awful/placement/bottom_left.lua +++ b/tests/examples/awful/placement/bottom_left.lua @@ -1,6 +1,7 @@ -- Align a client to the bottom left of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name bottom_left --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/bottom_right.lua b/tests/examples/awful/placement/bottom_right.lua index 0ad1072e..2abb4490 100644 --- a/tests/examples/awful/placement/bottom_right.lua +++ b/tests/examples/awful/placement/bottom_right.lua @@ -1,6 +1,7 @@ -- Align a client to the bottom right of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name bottom_right --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/center_horizontal.lua b/tests/examples/awful/placement/center_horizontal.lua index b9d1d6bb..931b947c 100644 --- a/tests/examples/awful/placement/center_horizontal.lua +++ b/tests/examples/awful/placement/center_horizontal.lua @@ -1,6 +1,7 @@ -- Align a client to the horizontal center left of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name center_horizontal --DOC_HEADER -- @class function --DOC_HEADER screen[1]._resize {width = 128, height = 96} --DOC_HIDE diff --git a/tests/examples/awful/placement/centered.lua b/tests/examples/awful/placement/centered.lua index af1a1c84..d6b1e3ae 100644 --- a/tests/examples/awful/placement/centered.lua +++ b/tests/examples/awful/placement/centered.lua @@ -1,6 +1,7 @@ -- Align a client to the center of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name centered --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/closest_mouse.lua b/tests/examples/awful/placement/closest_mouse.lua index 90642641..4269d1d8 100644 --- a/tests/examples/awful/placement/closest_mouse.lua +++ b/tests/examples/awful/placement/closest_mouse.lua @@ -55,7 +55,7 @@ assert(mouse.coords().x == c.x and mouse.coords().y == c.y+c.height+2*bw) --DOC_ -- It is possible to emulate the mouse API to get the closest corner of -- random area -local corner = awful.placement.closest_corner( +local _, corner = awful.placement.closest_corner( {coords=function() return {x = 100, y=100} end}, {include_sides = true, bounding_rect = {x=0, y=0, width=200, height=200}} ) diff --git a/tests/examples/awful/placement/compose.lua b/tests/examples/awful/placement/compose.lua index 28098677..f6710762 100644 --- a/tests/examples/awful/placement/compose.lua +++ b/tests/examples/awful/placement/compose.lua @@ -1,9 +1,11 @@ -screen[1]._resize {width = 128, height = 96} --DOC_HIDE +screen[1]._resize {x = 175, width = 128, height = 96} --DOC_NO_USAGE --DOC_HIDE local awful = {placement = require("awful.placement")} --DOC_HIDE -local c = client.gen_fake {x = 45, y = 35, width=40, height=30} --DOC_HIDE +local c = client.gen_fake {x = 220, y = 35, width=40, height=30} --DOC_HIDE -local f = (awful.placement.right + awful.placement.left) -f(client.focus) + -- "right" will be replaced by "left" + local f = (awful.placement.right + awful.placement.left) + f(client.focus) -assert(c.x == 0 and c.y==screen[1].geometry.height/2-30/2-c.border_width--DOC_HIDE +local sg = screen[1].geometry--DOC_HIDE +assert(c.x == sg.x and c.y==sg.height/2-30/2-c.border_width--DOC_HIDE and c.width==40 and c.height==30)--DOC_HIDE diff --git a/tests/examples/awful/placement/compose2.lua b/tests/examples/awful/placement/compose2.lua new file mode 100644 index 00000000..68cbf5b3 --- /dev/null +++ b/tests/examples/awful/placement/compose2.lua @@ -0,0 +1,19 @@ +screen[1]._resize {x = 175, width = 128, height = 96} --DOC_NO_USAGE --DOC_HIDE +local awful = {placement = require("awful.placement")} --DOC_HIDE +local c = client.gen_fake {x = 220, y = 35, width=40, height=30} --DOC_HIDE + + + -- Simulate Windows 7 "edge snap" (also called aero snap) feature + local axis = "vertically" + + local f = awful.placement.scale + + awful.placement.left + + (axis and awful.placement["maximize_"..axis] or nil) + + local geo = f(client.focus, {honor_workarea=true, to_percent = 0.5}) + +local wa = screen[1].workarea--DOC_HIDE +assert(c.x == wa.x and geo.x == wa.x)--DOC_HIDE +assert(c.y == wa.y) --DOC_HIDE +assert(c.width == wa.width/2 - 2*c.border_width)--DOC_HIDE +assert(c.height == wa.height - 2*c.border_width)--DOC_HIDE diff --git a/tests/examples/awful/placement/left.lua b/tests/examples/awful/placement/left.lua index 49cf93ac..d3e4e698 100644 --- a/tests/examples/awful/placement/left.lua +++ b/tests/examples/awful/placement/left.lua @@ -1,6 +1,7 @@ -- Align a client to the left of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name left --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/resize_to_mouse.lua b/tests/examples/awful/placement/resize_to_mouse.lua new file mode 100644 index 00000000..3114fb38 --- /dev/null +++ b/tests/examples/awful/placement/resize_to_mouse.lua @@ -0,0 +1,74 @@ +--DOC_HIDE_ALL +local awful = {placement = require("awful.placement")} +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +screen._setup_grid(64, 48, {4, 4, 4, 4}, {workarea_sides=0}) + +local function test_touch_mouse(c) + local coords = mouse.coords() + + return c:geometry().x == coords.x or c:geometry().y == coords.y + or c:geometry().x+c:geometry().width+2*c.border_width == coords.x + or c:geometry().y+c:geometry().height+2*c.border_width == coords.y +end + +for s=1, 8 do + local scr = screen[s] + local x, y = scr.geometry.x, scr.geometry.y + local c = client.gen_fake{x = x+22, y = y+16, width=20, height=15, screen=scr} + assert(client.get()[s] == c) +end + +for s=9, 16 do + local scr = screen[s] + local x, y = scr.geometry.x, scr.geometry.y + local c = client.gen_fake{x = x+10, y = y+10, width=44, height=28, screen=scr} + assert(client.get()[s] == c) +end + +local function move_corsor(s, x, y) + local sg = screen[s].geometry + mouse.coords {x=sg.x+x,y=sg.y+y} +end + +local all_coords_out = { + top_left = {10, 10}, + top = {32, 10}, + top_right = {60, 10}, + right = {60, 20}, + bottom_right = {60, 40}, + bottom = {32, 40}, + bottom_left = {10, 40}, + left = {10, 29}, +} + +local all_coords_in = { + top_left = {20, 18}, + top = {32, 18}, + top_right = {44, 18}, + right = {44, 24}, + bottom_right = {44, 34}, + bottom = {32, 34}, + bottom_left = {20, 34}, + left = {32, 24}, +} + +-- Top left +local s = 1 +for k, v in pairs(all_coords_out) do + move_corsor(s, unpack(v)) + assert(client.get()[s].screen == screen[s]) + awful.placement.resize_to_mouse(client.get()[s], {include_sides=true}) + mouse.push_history() + assert(test_touch_mouse(client.get()[s]), k) + s = s + 1 +end + +for k, v in pairs(all_coords_in) do + move_corsor(s, unpack(v)) + assert(client.get()[s].screen == screen[s]) + awful.placement.resize_to_mouse(client.get()[s], {include_sides=true}) + mouse.push_history() + assert(test_touch_mouse(client.get()[s]), k) + s = s + 1 +end diff --git a/tests/examples/awful/placement/right.lua b/tests/examples/awful/placement/right.lua index d2450116..4e16cea6 100644 --- a/tests/examples/awful/placement/right.lua +++ b/tests/examples/awful/placement/right.lua @@ -1,6 +1,7 @@ -- Align a client to the right of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name right --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/stretch_down.lua b/tests/examples/awful/placement/stretch_down.lua index 258647f8..720da36d 100644 --- a/tests/examples/awful/placement/stretch_down.lua +++ b/tests/examples/awful/placement/stretch_down.lua @@ -1,6 +1,7 @@ -- Stretch the drawable to the bottom of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name stretch_down --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/stretch_left.lua b/tests/examples/awful/placement/stretch_left.lua index 51364697..4fdd5857 100644 --- a/tests/examples/awful/placement/stretch_left.lua +++ b/tests/examples/awful/placement/stretch_left.lua @@ -1,6 +1,7 @@ -- Stretch the drawable to the left of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name stretch_left --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/stretch_right.lua b/tests/examples/awful/placement/stretch_right.lua index 9965d1a6..47dcce80 100644 --- a/tests/examples/awful/placement/stretch_right.lua +++ b/tests/examples/awful/placement/stretch_right.lua @@ -1,6 +1,7 @@ -- Stretch the drawable to the right of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name stretch_right --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/stretch_up.lua b/tests/examples/awful/placement/stretch_up.lua index 817a11d5..0ccfd2bc 100644 --- a/tests/examples/awful/placement/stretch_up.lua +++ b/tests/examples/awful/placement/stretch_up.lua @@ -1,6 +1,7 @@ -- Stretch the drawable to the top of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name stretch_up --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/top.lua b/tests/examples/awful/placement/top.lua index f84ea306..b3375fbc 100644 --- a/tests/examples/awful/placement/top.lua +++ b/tests/examples/awful/placement/top.lua @@ -1,6 +1,7 @@ -- Align a client to the top of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name top --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/top_left.lua b/tests/examples/awful/placement/top_left.lua index a2bb3b58..f70b85f6 100644 --- a/tests/examples/awful/placement/top_left.lua +++ b/tests/examples/awful/placement/top_left.lua @@ -1,6 +1,7 @@ -- Align a client to the top left of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name top_left --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/awful/placement/top_right.lua b/tests/examples/awful/placement/top_right.lua index a94890c9..ef6fc833 100644 --- a/tests/examples/awful/placement/top_right.lua +++ b/tests/examples/awful/placement/top_right.lua @@ -1,6 +1,7 @@ -- Align a client to the top right of the parent area. --DOC_HEADER -- @tparam drawable d A drawable (like `client`, `mouse` or `wibox`) --DOC_HEADER -- @tparam[opt={}] table args Other arguments") --DOC_HEADER +-- @treturn table The new geometry --DOC_HEADER -- @name top_right --DOC_HEADER -- @class function --DOC_HEADER diff --git a/tests/examples/shims/client.lua b/tests/examples/shims/client.lua index 97d19fd9..5fcc6486 100644 --- a/tests/examples/shims/client.lua +++ b/tests/examples/shims/client.lua @@ -166,6 +166,9 @@ client:_add_signal("property::fullscreen") client:_add_signal("property::border_width") client:_add_signal("property::hidden") client:_add_signal("property::screen") +client:_add_signal("request::tag") +client:_add_signal("request::geometry") +client:_add_signal("request::activate") client:_add_signal("raised") client:_add_signal("lowered") client:_add_signal("list") diff --git a/tests/test-miss-handlers.lua b/tests/test-miss-handlers.lua index f97d089b..fe61a860 100644 --- a/tests/test-miss-handlers.lua +++ b/tests/test-miss-handlers.lua @@ -1,5 +1,6 @@ -- Test set_{,new}index_miss_handler +local mouse = mouse local class = tag local obj = class({}) local handler = require("gears.object.properties") @@ -30,4 +31,8 @@ assert(not obj.key) obj.key = 1337 assert(obj.key == 1337) +-- The the custom mouse handler +mouse.foo = "bar" +assert(mouse.foo == "bar") + require("_runner").run_steps({ function() return true end }) diff --git a/tests/test-resize.lua b/tests/test-resize.lua new file mode 100644 index 00000000..a9eb7168 --- /dev/null +++ b/tests/test-resize.lua @@ -0,0 +1,409 @@ +local test_client = require("_client") +local placement = require("awful.placement") +local amouse = require("awful.mouse") + +local steps = {} + +table.insert(steps, function(count) + if count == 1 then -- Setup. + test_client("foobar", "foobar") + elseif #client.get() > 0 then + + client.get()[1] : geometry { + x = 200, + y = 200, + width = 300, + height = 300, + } + + return true + end +end) + +table.insert(steps, function() + -- The mousegrabber expect a button to be pressed. + root.fake_input("button_press",1) + local c = client.get()[1] + + -- Just in case there is an accidental delayed geometry callback + assert(c:geometry().x == 200) + assert(c:geometry().y == 200) + assert(c:geometry().width == 300) + assert(c:geometry().height == 300) + + mouse.coords {x = 500+2*c.border_width, y= 500+2*c.border_width} + + local corner = amouse.client.resize(c) + + assert(corner == "bottom_right") + + return true +end) + +-- The geometry should remain the same, as the cursor is placed at the end of +-- the geometry. +table.insert(steps, function() + + local c = client.get()[1] + + assert(c:geometry().x == 200) + assert(c:geometry().y == 200) + assert(c:geometry().width == 300) + assert(c:geometry().height == 300) + + mouse.coords {x = 600+2*c.border_width, y= 600+2*c.border_width} + + return true +end) + +-- Grow the client by 100px +table.insert(steps, function() + + local c = client.get()[1] + + assert(c:geometry().x == 200) + assert(c:geometry().y == 200) + assert(c:geometry().width == 400) + assert(c:geometry().height == 400) + + mouse.coords {x = 400+2*c.border_width, y= 400+2*c.border_width} + + return true +end) + +-- Shirnk the client by 200px +table.insert(steps, function() + + local c = client.get()[1] + + assert(c:geometry().x == 200) + assert(c:geometry().y == 200) + assert(c:geometry().width == 200) + assert(c:geometry().height == 200) + + mouse.coords {x = 100, y= 100} + + return true +end) + +-- Grow the client by 100px from the top left +table.insert(steps, function() + + local c = client.get()[1] + + assert(c:geometry().x == 100) + assert(c:geometry().y == 100) + assert(c:geometry().width == 300) + assert(c:geometry().height == 300) + + mouse.coords {x = 300, y= 200} + + return true +end) + +-- Shirnk the client by 100px from the top right +table.insert(steps, function() + + local c = client.get()[1] + + assert(c:geometry().x == 100) + assert(c:geometry().y == 200) +-- assert(c:geometry().width == 200-2*c.border_width) --FIXME off by border width... +-- assert(c:geometry().height == 200-2*c.border_width) --FIXME off by border width... + + mouse.coords {x = 300, y= 200} + + return true +end) + +-- Stop the resize +table.insert(steps, function() + root.fake_input("button_release",1) + +-- if not mousegrabber.isrunning then --FIXME it should work, but doesn't +-- return true +-- end + + mousegrabber.stop() + + return true +end) + +-- Setup for move testing +table.insert(steps, function() + assert(not mousegrabber.isrunning()) + + local c = client.get()[1] + + c:geometry { + width = 200, + height = 200, + } + + placement.bottom_right(c) + + mouse.coords {x = c.screen.geometry.width -150, + y = c.screen.geometry.height-150} + + + return true +end) + +-- Start the move mouse grabber +table.insert(steps, function() + local c = client.get()[1] + + -- The resize is over, it should not move the client anymore + assert(c:geometry().x == c.screen.geometry.width - 200 - 2*c.border_width) + assert(c:geometry().y == c.screen.geometry.height - 200 - 2*c.border_width) + assert(c:geometry().width == 200) + assert(c:geometry().height == 200) + + assert(c.valid) + + root.fake_input("button_press",1) + + -- Begin the move + amouse.client.move(c) + + -- Make sure nothing unwanted happen by accident + assert(c:geometry().x == c.screen.geometry.width - 200 - 2*c.border_width) + assert(c:geometry().y == c.screen.geometry.height - 200 - 2*c.border_width) + assert(c:geometry().width == 200) + assert(c:geometry().height == 200) + + -- The cursor should not have moved + assert(mouse.coords().x == c.screen.geometry.width - 150) + assert(mouse.coords().y == c.screen.geometry.height - 150) + mouse.coords {x = 50 + 2*c.border_width, y= 50 + 2*c.border_width} + + assert(mousegrabber.isrunning()) + + return true +end) + +-- The client should now be in the top left +table.insert(steps, function() + local c = client.get()[1] + assert(c:geometry().x == 0) + assert(c:geometry().y == 0) + assert(c:geometry().width == 200) + assert(c:geometry().height == 200) + + -- Move to the bottom left + mouse.coords { + x = 50 + 2*c.border_width, + y = c.screen.geometry.height - 200 + } + + return true +end) + +-- Ensure the move was correct, the snap to the top part of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == 0) + assert(c:geometry().y == c.screen.geometry.height - 250 - 2*c.border_width) + assert(c:geometry().width == 200) + assert(c:geometry().height == 200) + + -- Should trigger the top snap + mouse.coords {x = 600, y= 0} + + -- The snap is only applied on release + root.fake_input("button_release",1) + + return true +end) + +-- The client should now fill the top half of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == c.screen.workarea.x ) + assert(c:geometry().y == c.screen.workarea.y ) + assert(c:geometry().width == c.screen.workarea.width - 2*c.border_width ) + assert(c:geometry().height == math.ceil(c.screen.workarea.height/2 - 2*c.border_width)) + + -- Snap to the top right + root.fake_input("button_press",1) + amouse.client.move(c) + placement.top_right(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The client should now fill the top right corner of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == c.screen.workarea.x+c.screen.workarea.width/2 ) + assert(c:geometry().y == c.screen.workarea.y ) + assert(c:geometry().width == math.ceil(c.screen.workarea.width/2 - 2*c.border_width) ) + assert(c:geometry().height == math.ceil(c.screen.workarea.height/2 - 2*c.border_width)) + + -- Snap to the top right + root.fake_input("button_press",1) + amouse.client.move(c) + placement.right(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The client should now fill the top right half of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == c.screen.workarea.x+c.screen.workarea.width/2 ) + assert(c:geometry().y == c.screen.workarea.y ) + assert(c:geometry().width == math.ceil(c.screen.workarea.width/2 - 2*c.border_width)) + assert(c:geometry().height == c.screen.workarea.height - 2*c.border_width ) + + -- Snap to the top right + root.fake_input("button_press",1) + amouse.client.move(c) + placement.bottom(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The client should now fill the bottom half of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == c.screen.workarea.x ) + assert(c:geometry().y == c.screen.workarea.y+c.screen.workarea.height/2 ) + assert(c:geometry().width == c.screen.workarea.width - 2*c.border_width ) + assert(c:geometry().height == math.ceil(c.screen.workarea.height/2 - 2*c.border_width)) + + -- Snap to the top right + root.fake_input("button_press",1) + amouse.client.move(c) + placement.bottom_left(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The client should now fill the bottom left corner of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == c.screen.workarea.x ) + assert(c:geometry().y == c.screen.workarea.y+c.screen.workarea.height/2 ) + assert(c:geometry().width == math.ceil(c.screen.workarea.width/2 - 2*c.border_width) ) + assert(c:geometry().height == math.ceil(c.screen.workarea.height/2 - 2*c.border_width)) + + -- Snap to the top right + root.fake_input("button_press",1) + amouse.client.move(c) + placement.left(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The client should now fill the left half of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == c.screen.workarea.x ) + assert(c:geometry().y == c.screen.workarea.y ) + assert(c:geometry().width == math.ceil(c.screen.workarea.width/2 - 2*c.border_width) ) + assert(c:geometry().height == c.screen.workarea.height - 2*c.border_width ) + + -- Snap to the top right + root.fake_input("button_press",1) + amouse.client.move(c) + placement.top_left(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +local cur_tag = nil + +-- The client should now fill the top left corner of the screen +table.insert(steps, function() + local c = client.get()[1] + + assert(c:geometry().x == c.screen.workarea.x ) + assert(c:geometry().y == c.screen.workarea.y ) + assert(c:geometry().width == math.ceil(c.screen.workarea.width/2 - 2*c.border_width) ) + assert(c:geometry().height == math.ceil(c.screen.workarea.height/2 - 2*c.border_width)) + + -- Change the mode to test drag_to_tag + amouse.drag_to_tag.enabled = true + amouse.snap.edge_enabled = false + + cur_tag = c.first_tag + + root.fake_input("button_press",1) + amouse.client.move(c) + placement.right(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The tag should have changed +table.insert(steps, function() + local c = client.get()[1] + + assert(c.first_tag ~= cur_tag ) + assert(c.first_tag.index == cur_tag.index + 1) + + -- Move it back + root.fake_input("button_press",1) + amouse.client.move(c) + placement.left(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The tag should now be the same as before +table.insert(steps, function() + local c = client.get()[1] + + assert(c.first_tag == cur_tag) + assert(c.first_tag.index == 1) + + -- Wrap + root.fake_input("button_press",1) + amouse.client.move(c) + placement.left(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The tag should now be the last +table.insert(steps, function() + local c = client.get()[1] + + assert(c.first_tag.index == #c.screen.tags) + + -- Wrap back + root.fake_input("button_press",1) + amouse.client.move(c) + placement.right(mouse, {honor_workarea=false}) + root.fake_input("button_release",1) + + return true +end) + +-- The tag should now original one again +table.insert(steps, function() + local c = client.get()[1] + + assert(c.first_tag == cur_tag) + + return true +end) + +require("_runner").run_steps(steps)