--------------------------------------------------------------------------- -- @author Julien Danjou <julien@danjou.info> -- @copyright 2008 Julien Danjou -- @release @AWESOME_VERSION@ --------------------------------------------------------------------------- -- Grab environment we need local util = require("awful.util") local tag = require("awful.tag") local pairs = pairs local ipairs = ipairs local table = table local math = math local otable = otable local setmetatable = setmetatable local capi = { client = client, mouse = mouse, screen = screen, hooks = hooks } local hooks = require("awful.hooks") --- Client module for awful module("awful.client") -- Private data local data = {} data.focus = {} data.urgent = {} data.marked = {} data.properties = otable() -- Functions urgent = {} focus = {} focus.history = {} swap = {} floating = {} property = {} -- User hooks hooks.user.create('marked') hooks.user.create('unmarked') --- Get the first client that got the urgent hint. -- @return The first urgent client. function urgent.get() if #data.urgent > 0 then return data.urgent[1] else -- fallback behaviour: iterate through clients and get the first urgent local clients = capi.client.get() for k, cl in pairs(clients) do if cl.urgent then return cl end end end end --- Jump to the client that received the urgent hint first. function urgent.jumpto() local c = urgent.get() if c then local s = capi.client.focus and capi.client.focus.screen or capi.mouse.screen -- focus the screen if s ~= c.screen then capi.mouse.screen = c.screen end -- focus the tag tag.viewonly(c:tags()[1]) -- focus the client capi.client.focus = c c:raise() end end --- Adds client to urgent stack. -- @param c The client object. -- @param prop The property which is updated. function urgent.add(c, prop) if prop == "urgent" and c.urgent then table.insert(data.urgent, c) end end --- Remove client from urgent stack. -- @param c The client object. function urgent.delete(c) for k, cl in ipairs(data.urgent) do if c == cl then table.remove(data.urgent, k) break end end end --- Remove a client from the focus history -- @param c The client that must be removed. function focus.history.delete(c) for k, v in ipairs(data.focus) do if v == c then table.remove(data.focus, k) break end end end --- Filter out window that we do not want handled by focus. -- This usually means that desktop, dock and splash windows are -- not registered and cannot get focus. -- @param c A client. -- @return The same client if it's ok, nil otherwise. function focus.filter(c) if c.type == "desktop" or c.type == "dock" or c.type == "splash" then return nil end return c end --- Update client focus history. -- @param c The client that has been focused. function focus.history.add(c) if focus.filter(c) then -- Remove the client if its in stack focus.history.delete(c) -- Record the client has latest focused table.insert(data.focus, 1, c) end end --- Get the latest focused client for a screen in history. -- @param screen The screen number to look for. -- @param idx The index: 0 will return first candidate, -- 1 will return second, etc. -- @return A client. function focus.history.get(screen, idx) -- When this counter is equal to idx, we return the client local counter = 0 local vc = visible(screen) for k, c in ipairs(data.focus) do if c.screen == screen then for j, vcc in ipairs(vc) do if vcc == c then if counter == idx then return c end -- We found one, increment the counter only. counter = counter + 1 break end end end end -- Argh nobody found in history, give the first one visible if there is one -- that passes the filter. if counter == 0 then for k, v in ipairs(vc) do if focus.filter(v) then return v end end end end --- Focus the previous client in history. function focus.history.previous() local sel = capi.client.focus local s if sel then s = sel.screen else s = capi.mouse.screen end local c = focus.history.get(s, 1) if c then capi.client.focus = c end end --- Get visible clients from a screen. -- @param screen The screen number, or nil for all screens. -- @return A table with all visible clients. function visible(screen) local cls = capi.client.get(screen) local vcls = {} for k, c in pairs(cls) do if c:isvisible() then table.insert(vcls, c) end end return vcls end --- Get visible and tiled clients -- @param screen The screen number, or nil for all screens. -- @return A tabl with all visible and tiled clients. function tiled(screen) local clients = visible(screen) local tclients = {} -- Remove floating clients for k, c in pairs(clients) do if not floating.get(c) then table.insert(tclients, c) end end return tclients end --- Get a client by its relative index to the focused window. -- @usage Set i to 1 to get next, -1 to get previous. -- @param i The index. -- @param c Optional client. -- @return A client, or nil if no client is available. function next(i, c) -- Get currently focused client local sel = c or capi.client.focus if sel then -- Get all visible clients local cls = visible(sel.screen) local fcls = {} -- Remove all non-normal clients for idx, c in ipairs(cls) do if focus.filter(c) or c == sel then table.insert(fcls, c) end end cls = fcls -- Loop upon each client for idx, c in ipairs(cls) do if c == sel then -- Cycle return cls[util.cycle(#cls, idx + i)] end end end end -- Return true whether client B is in the right direction -- compared to client A. -- @param dir The direction. -- @param cA The first client. -- @param cB The second client. -- @return True if B is in the direction of A. local function is_in_direction(dir, cA, cB) if dir == "up" then return cA['y'] > cB['y'] elseif dir == "down" then return cA['y'] < cB['y'] elseif dir == "left" then return cA['x'] > cB['x'] elseif dir == "right" then return cA['x'] < cB['x'] end return false end -- Calculate distance between two points. -- i.e: if we want to move to the right, we will take the right border -- of the currently focused client and the left side of the checked client. -- This avoid the focus of an upper client when you move to the right in a -- tilebottom layout with nmaster=2 and 5 clients open, for instance. -- @param dir The direction. -- @param cA The first client. -- @param cB The second client. -- @return The distance between the clients. local function calculate_distance(dir, cA, cB) local xA = cA['x'] local xB = cB['x'] local yA = cA['y'] local yB = cB['y'] if dir == "up" then yB = yB + cB['height'] elseif dir == "down" then yA = yA + cA['height'] elseif dir == "left" then xB = xB + cB['width'] elseif dir == "right" then xA = xA + cA['width'] end return math.sqrt(math.pow(xB - xA, 2) + math.pow(yB - yA, 2)) end -- Get the nearest client in the given direction -- @param dir The direction, can be either "up", "down", "left" or "right". -- @param c Optional client to get a client relative to. Else focussed is used. local function get_client_in_direction(dir, c) local sel = c or capi.client.focus if sel then local geometry = sel:geometry() local dist, dist_min local target = nil local cls = visible(sel.screen) -- We check each client. for i, c in ipairs(cls) do -- Check geometry to see if client is located in the right direction. if is_in_direction(dir, geometry, c:geometry()) then -- Calculate distance between focused client and checked client. dist = calculate_distance(dir, geometry, c:geometry()) -- If distance is shorter then keep the client. if not target or dist < dist_min then target = c dist_min = dist end end end return target end end --- Focus a client by the given direction. -- @param dir The direction, can be either "up", "down", "left" or "right". -- @param c Optional client. function focus.bydirection(dir, c) local sel = c or capi.client.focus if sel then local target = get_client_in_direction(dir, sel) -- If we found a client to focus, then do it. if target then capi.client.focus = target end end end --- Focus a client by its relative index. -- @param i The index. -- @param c Optional client. function focus.byidx(i, c) local target = next(i, c) if target then capi.client.focus = target end end --- Swap a client with another client in the given direction -- @param dir The direction, can be either "up", "down", "left" or "right". -- @param c Optional client. function swap.bydirection(dir, c) local sel = c or capi.client.focus if sel then local target = get_client_in_direction(dir, sel) -- If we found a client to swap with, then go for it if target then target:swap(sel) end end end --- Swap a client by its relative index. -- @param i The index. -- @param c Optional client, otherwise focused one is used. function swap.byidx(i, c) local sel = c or capi.client.focus local target = next(i, sel) if target then target:swap(sel) end end --- Get the master window. -- @param screen Optional screen number, otherwise screen mouse is used. -- @return The master window. function getmaster(screen) local s = screen or capi.mouse.screen return visible(s)[1] end --- Set the client as slave: put it at the end of other windows. -- @param c The window to set as slave. function setslave(c) local cls = visible(c.screen) for k, v in pairs(cls) do c:swap(v) end end --- Move/resize a client relative to current coordinates. -- @param x The relative x coordinate. -- @param y The relative y coordinate. -- @param w The relative width. -- @param h The relative height. -- @param c The optional client, otherwise focused one is used. function moveresize(x, y, w, h, c) local sel = c or capi.client.focus local geometry = sel:geometry() geometry['x'] = geometry['x'] + x geometry['y'] = geometry['y'] + y geometry['width'] = geometry['width'] + w geometry['height'] = geometry['height'] + h sel:geometry(geometry) end function maximize() util.deprecate("maximized_horizontal and vertical client attributes") end --- Move a client to a tag. -- @param target The tag to move the client to. -- @param c Optional client to move, otherwise the focused one is used. function movetotag(target, c) local sel = c or capi.client.focus if sel then -- Check that tag and client screen are identical if sel.screen ~= target.screen then return end sel:tags({ target }) end end --- Toggle a tag on a client. -- @param target The tag to toggle. -- @param c Optional client to toggle, otherwise the focused one is used. function toggletag(target, c) local sel = c or capi.client.focus -- Check that tag and client screen are identical if sel and sel.screen == target.screen then local tags = sel:tags() if tags[target] then -- If it's the only tag for the window, stop. if #tags == 1 then return end tags[tags[target]] = nil else tags[target] = target end sel:tags(tags) end end --- Move a client to a screen. Default is next screen, cycling. -- @param c The client to move. -- @param s The screen number, default to current + 1. function movetoscreen(c, s) local sel = c or capi.client.focus if sel then local sc = capi.screen.count() if not s then s = sel.screen + 1 end if s > sc then s = 1 elseif s < 1 then s = sc end sel.screen = s capi.mouse.coords(capi.screen[s].geometry) capi.client.focus = sel end end --- Mark a client, and then call 'marked' hook. -- @param c The client to mark, the focused one if not specified. -- @return True if the client has been marked. False if the client was already marked. function mark(c) local cl = c or capi.client.focus if cl then for k, v in pairs(data.marked) do if cl == v then return false end end table.insert(data.marked, cl) -- Call callback hooks.user.call('marked', cl) return true end end --- Unmark a client and then call 'unmarked' hook. -- @param c The client to unmark, or the focused one if not specified. -- @return True if the client has been unmarked. False if the client was not marked. function unmark(c) local cl = c or capi.client.focus for k, v in pairs(data.marked) do if cl == v then table.remove(data.marked, k) hooks.user.call('unmarked', cl) return true end end return false end --- Check if a client is marked. -- @param c The client to check, or the focused one otherwise. function ismarked(c) local cl = c or capi.client.focus if cl then for k, v in pairs(data.marked) do if cl == v then return true end end end return false end --- Toggle a client as marked. -- @param c The client to toggle mark. function togglemarked(c) local cl = c or capi.client.focus if not mark(c) then unmark(c) end end --- Return the marked clients and empty the marked table. -- @return A table with all marked clients. function getmarked() for k, v in pairs(data.marked) do hooks.user.call('unmarked', v) end t = data.marked data.marked = {} return t end --- Set a client floating state, overriding auto-detection. -- Floating client are not handled by tiling layouts. -- @param c A client. -- @param state True or false. function floating.set(c, s) local c = c or capi.client.focus if c and property.get(c, "floating") ~= s then property.set(c, "floating", s) capi.hooks.arrange()(c.screen) capi.hooks.property()(c, "floating") end end --- Return if a client has a fixe size or not. -- @param c The client. function isfixed(c) local c = c or capi.client.focus if not c then return end local h = c.size_hints if h.min_width and h.max_width and h.max_height and h.min_height and h.min_width > 0 and h.max_width > 0 and h.max_height > 0 and h.min_height > 0 and h.min_width == h.max_width and h.min_height == h.max_height then return true end return false end --- Get a client floating state. -- @param c A client. -- @return True or false. Note that some windows might be floating even if you -- did not set them manually. For example, windows with a type different than -- normal. function floating.get(c) local c = c or capi.client.focus if c then local value = property.get(c, "floating") if value ~= nil then return value end if c.type ~= "normal" or c.fullscreen or c.maximized_vertical or c.maximized_horizontal or isfixed(c) then return true end return false end end --- Toggle the floating state of a client between 'auto' and 'true'. -- @param c A client. function floating.toggle(c) -- If it has been set to floating if property.get(c, "floating") then floating.set(c, nil) else floating.set(c, true) end end --- Remove the floating information on a client. -- @param c The client. function floating.delete(c) floating.set(c, nil) end -- Normalize a set of numbers to 1 -- @param set the set of numbers to normalize -- @param num the number of numbers to normalize local function normalize(set, num) local num = num or #set local total = 0 if num then for i = 1,num do total = total + set[i] end for i = 1,num do set[i] = set[i] / total end else for i,v in ipairs(set) do total = total + v end for i,v in ipairs(set) do set[i] = v / total end end end --- Calculate a client's column number, index in that column, and -- number of visible clients in this column. -- @param c the client -- @return col the column number -- @return idx index of the client in the column -- @return num the number of visible clients in the column local function idx(c) local c = c or capi.client.focus if not c then return end local clients = tiled(c.screen) local idx = nil for k, cl in ipairs(clients) do if cl == c then idx = k break end end local nmaster = tag.getnmaster(t) if idx <= nmaster then return {idx = idx, col=0, num=nmaster} end local nother = #clients - nmaster idx = idx - nmaster -- rather than regenerate the column number we can calculate it -- based on the how the tiling algorithm places clients we calculate -- the column, we could easily use the for loop in the program but we can -- calculate it. local ncol = tag.getncol(t) -- minimum number of clients per column local percol = math.floor(nother / ncol) -- number of columns with an extra client local overcol = math.mod(nother, ncol) -- number of columns filled with [percol] clients local regcol = ncol - overcol local col = math.floor( (idx - 1) / percol) + 1 if col > regcol then -- col = math.floor( (idx - (percol*regcol) - 1) / (percol + 1) ) + regcol + 1 -- simplified col = math.floor( (idx + regcol + percol) / (percol+1) ) -- calculate the index in the column idx = idx - percol*regcol - (col - regcol - 1) * (percol+1) percol = percol+1 else idx = idx - percol*(col-1) end return {idx = idx, col=col, num=percol} end --- Set the window factor of a client -- @param wfact the window factor value -- @param c the client function setwfact(wfact, c) -- get the currently selected window local c = c or capi.client.focus if not c then return end local t = tag.selected(c.screen) local w = idx(c) local cls = tiled(t.screen) local nmaster = tag.getnmaster(t) -- n is the number of windows currently visible for which we have to be concerned with the properties local data = tag.getproperty(t, "windowfact") or {} local colfact = data[w.col] colfact[w.idx] = wfact rest = 1-wfact -- calculate the current denominator local total = 0 for i = 1,w.num do if i ~= w.idx then total = total + colfact[i] end end -- normalize the windows for i = 1,w.num do if i ~= w.idx then colfact[i] = (colfact[i] * rest) / total end end end --- Increment a client's window factor -- @param add amount to increase the client's window -- @param c the client function incwfact(add, c) local c = c or capi.client.focus if not c then return end local t = tag.selected(c.screen) local w = idx(c) local nmaster = tag.getnmaster(t) local data = tag.getproperty(t, "windowfact") or {} local colfact = data[w.col] curr = colfact[w.idx] or 1 colfact[w.idx] = curr + add -- keep our ratios normalized normalize(colfact, w.num) capi.hooks.arrange()(t.screen) end --- Get a client property. -- @param c The client. -- @param prop The property name. -- @return The property. function property.get(c, prop) if data.properties[c] then return data.properties[c][prop] end end --- Set a client property. -- This properties are internal to awful. Some are used to move clients, etc. -- @param c The client. -- @param prop The property name. -- @param value The value. function property.set(c, prop, value) if not data.properties[c] then data.properties[c] = {} end data.properties[c][prop] = value end -- Kill client property. -- This is garbage collection done on unmanage. -- @param c The client. local function killproperty(c) data.properties[c] = nil end -- Register standards hooks hooks.focus.register(focus.history.add) hooks.unmanage.register(focus.history.delete) hooks.property.register(urgent.add) hooks.focus.register(urgent.delete) hooks.unmanage.register(urgent.delete) hooks.unmanage.register(floating.delete) hooks.unmanage.register(killproperty) -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80