1---------------------------------------------------------------------------
2--- An extendable mouse resizing handler.
3--
4-- This module offers a resizing and moving mechanism for drawables such as
5-- clients and wiboxes.
6--
7-- @author Emmanuel Lepage Vallee <elv1313@gmail.com>
8-- @copyright 2016 Emmanuel Lepage Vallee
9-- @submodule mouse
10---------------------------------------------------------------------------
11
12local aplace = require("awful.placement")
13local capi = {mousegrabber = mousegrabber}
14local beautiful = require("beautiful")
15
16local module = {}
17
18local mode      = "live"
19local req       = "request::geometry"
20local callbacks = {enter={}, move={}, leave={}}
21
22local cursors = {
23    ["mouse.move"               ] = "fleur",
24    ["mouse.resize"             ] = "cross",
25    ["mouse.resize_left"        ] = "sb_h_double_arrow",
26    ["mouse.resize_right"       ] = "sb_h_double_arrow",
27    ["mouse.resize_top"         ] = "sb_v_double_arrow",
28    ["mouse.resize_bottom"      ] = "sb_v_double_arrow",
29    ["mouse.resize_top_left"    ] = "top_left_corner",
30    ["mouse.resize_top_right"   ] = "top_right_corner",
31    ["mouse.resize_bottom_left" ] = "bottom_left_corner",
32    ["mouse.resize_bottom_right"] = "bottom_right_corner",
33}
34
35--- The resize cursor name.
36-- @beautiful beautiful.cursor_mouse_resize
37-- @tparam[opt=cross] string cursor
38
39--- The move cursor name.
40-- @beautiful beautiful.cursor_mouse_move
41-- @tparam[opt=fleur] string cursor
42
43--- Set the resize mode.
44-- The available modes are:
45--
46-- * **live**: Resize the layout everytime the mouse moves.
47-- * **after**: Resize the layout only when the mouse is released.
48--
49-- Some clients, such as XTerm, may lose information if resized too often.
50--
51-- @function awful.mouse.resize.set_mode
52-- @tparam string m The mode
53function module.set_mode(m)
54    assert(m == "live" or m == "after")
55    mode = m
56end
57
58--- Add an initialization callback.
59-- This callback will be executed before the mouse grabbing starts.
60-- @function awful.mouse.resize.add_enter_callback
61-- @tparam function cb The callback (or nil)
62-- @tparam[default=other] string context The callback context
63function module.add_enter_callback(cb, context)
64    context = context or "other"
65    callbacks.enter[context] = callbacks.enter[context] or {}
66    table.insert(callbacks.enter[context], cb)
67end
68
69--- Add a "move" callback.
70-- This callback is executed in "after" mode (see `set_mode`) instead of
71-- applying the operation.
72-- @function awful.mouse.resize.add_move_callback
73-- @tparam function cb The callback (or nil)
74-- @tparam[default=other] string context The callback context
75function module.add_move_callback(cb, context)
76    context = context or  "other"
77    callbacks.move[context] = callbacks.move[context]  or {}
78    table.insert(callbacks.move[context], cb)
79end
80
81--- Add a "leave" callback
82-- This callback is executed just before the `mousegrabber` stop
83-- @function awful.mouse.resize.add_leave_callback
84-- @tparam function cb The callback (or nil)
85-- @tparam[default=other] string context The callback context
86function module.add_leave_callback(cb, context)
87    context = context or  "other"
88    callbacks.leave[context] = callbacks.leave[context]  or {}
89    table.insert(callbacks.leave[context], cb)
90end
91
92--- Resize the drawable.
93--
94-- Valid `args` are:
95--
96-- * *enter_callback*: A function called before the `mousegrabber` starts.
97-- * *move_callback*: A function called when the mouse moves.
98-- * *leave_callback*: A function called before the `mousegrabber` is released.
99-- * *mode*: The resize mode.
100--
101-- @function awful.mouse.resize
102-- @tparam client client A client.
103-- @tparam[default=mouse.resize] string context The resizing context.
104-- @tparam[opt={}] table args A set of `awful.placement` arguments.
105
106local function handler(_, client, context, args) --luacheck: no unused_args
107    args = args or {}
108    context = context or "mouse.resize"
109
110    local placement = args.placement
111
112    if type(placement) == "string" and aplace[placement] then
113        placement = aplace[placement]
114    end
115
116    -- Extend the table with the default arguments
117    args = setmetatable(
118        {
119            placement = placement or aplace.resize_to_mouse,
120            mode      = args.mode or mode,
121            pretend   = true,
122        },
123        {__index = args or {}}
124    )
125
126    local geo
127
128    for _, cb in ipairs(callbacks.enter[context] or {}) do
129        geo = cb(client, args)
130
131        if geo == false then
132            return false
133        end
134    end
135
136    if args.enter_callback then
137        geo = args.enter_callback(client, args)
138
139        if geo == false then
140            return false
141        end
142    end
143
144    geo = nil
145
146    -- Select the cursor
147    local tcontext = context:gsub('[.]', '_')
148    local corner = args.corner and ("_".. args.corner) or ""
149
150    local cursor = beautiful["cursor_"..tcontext]
151        or cursors[context..corner]
152        or cursors[context]
153        or "fleur"
154
155    -- Execute the placement function and use request::geometry
156    capi.mousegrabber.run(function (_mouse)
157        if not client.valid then return end
158
159        -- Resize everytime the mouse moves (default behavior) in live mode,
160        -- otherwise keep the current geometry
161        geo = setmetatable(
162            args.mode == "live" and args.placement(client, args) or client:geometry(),
163            {__index=args}
164        )
165
166        -- Execute the move callbacks. This can be used to add features such as
167        -- snap or adding fancy graphical effects.
168        for _, cb in ipairs(callbacks.move[context] or {}) do
169            -- If something is returned, assume it is a modified geometry
170            geo = cb(client, geo, args) or geo
171
172            if geo == false then
173                return false
174            end
175        end
176
177        if args.move_callback then
178            geo = args.move_callback(client, geo, args)
179
180            if geo == false then
181                return false
182            end
183        end
184
185        -- In case it was modified
186        if geo then
187            setmetatable(geo, {__index=args})
188        end
189
190        if args.mode == "live" then
191            -- Ask the resizing handler to resize the client
192            client:emit_signal(req, context, geo)
193        end
194
195        -- Quit when the button is released
196        for _,v in pairs(_mouse.buttons) do
197            if v then return true end
198        end
199
200        -- Only resize after the mouse is released, this avoids losing content
201        -- in resize sensitive apps such as XTerm or allows external modules
202        -- to implement custom resizing.
203        if args.mode == "after" then
204            -- Get the new geometry
205            geo = args.placement(client, args)
206
207            -- Ask the resizing handler to resize the client
208            client:emit_signal(req, context, geo)
209        end
210
211        geo = nil
212
213        for _, cb in ipairs(callbacks.leave[context] or {}) do
214            geo = cb(client, geo, args)
215        end
216
217        if args.leave_callback then
218            geo = args.leave_callback(client, geo, args)
219        end
220
221        if not geo then return false end
222
223        -- In case it was modified
224        setmetatable(geo,{__index=args})
225
226        client:emit_signal(req, context, geo)
227
228        return false
229    end, cursor)
230end
231
232return setmetatable(module, {__call=handler})
233
234-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
235