264 lines
8.4 KiB
Lua
264 lines
8.4 KiB
Lua
--- === hs.hints ===
|
|
---
|
|
--- Switch focus with a transient per-application keyboard shortcut
|
|
|
|
local hints = require "hs.libhints"
|
|
local window = require "hs.window"
|
|
local hotkey = require "hs.hotkey"
|
|
local modal_hotkey = hotkey.modal
|
|
|
|
--- hs.hints.hintChars
|
|
--- Variable
|
|
--- This controls the set of characters that will be used for window hints. They must be characters found in hs.keycodes.map
|
|
--- The default is the letters A-Z. Note that if `hs.hints.style` is set to "vimperator", this variable will be ignored.
|
|
hints.hintChars = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}
|
|
|
|
-- vimperator mode requires to use full set of alphabet to represent applications.
|
|
hints.hintCharsVimperator = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}
|
|
|
|
--- hs.hints.style
|
|
--- Variable
|
|
--- If this is set to "vimperator", every window hint starts with the first character
|
|
--- of the parent application's title
|
|
hints.style = "default"
|
|
|
|
--- hs.hints.fontName
|
|
--- Variable
|
|
--- A fully specified family-face name, preferrably the PostScript name, such as Helvetica-BoldOblique or Times-Roman. (The Font Book app displays PostScript names of fonts in the Font Info panel.)
|
|
--- The default value is the system font
|
|
hints.fontName = nil
|
|
|
|
--- hs.hints.fontSize
|
|
--- Variable
|
|
--- The size of font that should be used. A value of 0.0 will use the default size.
|
|
hints.fontSize = 0.0
|
|
|
|
--- hs.hints.showTitleThresh
|
|
--- Variable
|
|
--- If there are less than or equal to this many windows on screen their titles will be shown in the hints.
|
|
--- The default is 4. Setting to 0 will disable this feature.
|
|
hints.showTitleThresh = 4
|
|
|
|
--- hs.hints.titleMaxSize
|
|
--- Variable
|
|
--- If the title is longer than maxSize, the string is truncated, -1 to disable, valid value is >= 6
|
|
hints.titleMaxSize = -1
|
|
|
|
--- hs.hints.iconAlpha
|
|
--- Variable
|
|
--- Opacity of the application icon. Default is 0.95.
|
|
hints.iconAlpha = 0.95
|
|
|
|
local openHints = {}
|
|
local takenPositions = {}
|
|
local hintDict = {}
|
|
local hintChars = nil
|
|
local modalKey = nil
|
|
local selectionCallback = nil
|
|
|
|
local bumpThresh = 40^2
|
|
local bumpMove = 80
|
|
|
|
local invalidWindowRoles = {
|
|
AXScrollArea = true, --This excludes the main finder window.
|
|
AXUnknown = true
|
|
}
|
|
|
|
local function isValidWindow(win, allowNonStandard)
|
|
if not allowNonStandard then
|
|
return win:isStandard()
|
|
else
|
|
return invalidWindowRoles[win:role()] == nil
|
|
end
|
|
end
|
|
|
|
function hints.bumpPos(x,y)
|
|
for _, pos in ipairs(takenPositions) do
|
|
if ((pos.x-x)^2 + (pos.y-y)^2) < bumpThresh then
|
|
return hints.bumpPos(x,y+bumpMove)
|
|
end
|
|
end
|
|
return {x = x,y = y}
|
|
end
|
|
|
|
function hints.addWindow(dict, win)
|
|
local n = dict['count']
|
|
if n == nil then
|
|
dict['count'] = 0
|
|
n = 0
|
|
end
|
|
local m = (n % #hintChars) + 1
|
|
local char = hintChars[m]
|
|
if n < #hintChars then
|
|
dict[char] = win
|
|
else
|
|
if type(dict[char]) == "userdata" then
|
|
-- dict[m] is already occupied by another window
|
|
-- which me must convert into a new dictionary
|
|
local otherWindow = dict[char]
|
|
dict[char] = {}
|
|
hints.addWindow(dict, otherWindow)
|
|
end
|
|
hints.addWindow(dict[char], win)
|
|
end
|
|
dict['count'] = dict['count'] + 1
|
|
end
|
|
|
|
-- Private helper to recursively find the total number of hints in a dict
|
|
function hints._dictSize(t)
|
|
if type(t) == "userdata" and t:screen() then -- onscreen window
|
|
return 1
|
|
elseif type(t) == "table" then
|
|
local count = 0
|
|
for _,v in pairs(t) do count = count + hints._dictSize(v) end
|
|
return count
|
|
end
|
|
return 0 -- screenless window or something else
|
|
end
|
|
|
|
function hints.displayHintsForDict(dict, prefixstring, showTitles, allowNonStandard)
|
|
if showTitles == nil then
|
|
showTitles = hints._dictSize(hintDict) <= hints.showTitleThresh
|
|
end
|
|
for key, val in pairs(dict) do
|
|
if type(val) == "userdata" and val:screen() then -- this is an onscreen window
|
|
local win = val
|
|
local app = win:application()
|
|
local fr = win:frame()
|
|
local sfr = win:screen():frame()
|
|
if app and app:bundleID() and isValidWindow(win, allowNonStandard) then
|
|
local c = {x = fr.x + (fr.w/2) - sfr.x, y = fr.y + (fr.h/2) - sfr.y}
|
|
local d = hints.bumpPos(c.x, c.y)
|
|
if d.y > (sfr.y + sfr.h - bumpMove) then
|
|
d.x = d.x + bumpMove
|
|
d.y = fr.y + (fr.h/2) - sfr.y
|
|
d = hints.bumpPos(d.x, d.y)
|
|
end
|
|
c = d
|
|
if c.y < 0 then
|
|
print("hs.hints: Skipping offscreen window: "..win:title())
|
|
else
|
|
local suffixString = ""
|
|
if showTitles then
|
|
local win_title = win:title()
|
|
if hints.titleMaxSize > 1 and #win_title > hints.titleMaxSize then
|
|
local end_idx = math.max(0, hints.titleMaxSize-3)
|
|
win_title = string.sub(win_title, 1, end_idx) .. "..."
|
|
end
|
|
suffixString = ": "..win_title
|
|
end
|
|
-- print(win:title().." x:"..c.x.." y:"..c.y) -- debugging
|
|
local hint = hints.new(c.x, c.y, prefixstring .. key .. suffixString, app:bundleID(), win:screen(), hints.fontName, hints.fontSize, hints.iconAlpha)
|
|
table.insert(takenPositions, c)
|
|
table.insert(openHints, hint)
|
|
end
|
|
end
|
|
elseif type(val) == "table" then -- this is another window dict
|
|
hints.displayHintsForDict(val, prefixstring .. key, showTitles, allowNonStandard)
|
|
end
|
|
end
|
|
end
|
|
|
|
function hints.processChar(char)
|
|
local toFocus = nil
|
|
|
|
if hintDict[char] ~= nil then
|
|
hints.closeHints()
|
|
if type(hintDict[char]) == "userdata" then
|
|
if hintDict[char] then
|
|
toFocus = hintDict[char]
|
|
end
|
|
elseif type(hintDict[char]) == "table" then
|
|
hintDict = hintDict[char]
|
|
if hintDict.count == 1 then
|
|
toFocus = hintDict[hintChars[1]]
|
|
else
|
|
takenPositions = {}
|
|
hints.displayHintsForDict(hintDict, "")
|
|
end
|
|
end
|
|
end
|
|
|
|
if toFocus then
|
|
toFocus:focus()
|
|
modalKey:exit()
|
|
if selectionCallback then
|
|
selectionCallback(toFocus)
|
|
end
|
|
end
|
|
end
|
|
|
|
function hints.setupModal()
|
|
local k = modal_hotkey.new(nil, nil)
|
|
k:bind({}, 'escape', function() hints.closeHints(); k:exit() end)
|
|
|
|
for _, c in ipairs(hintChars) do
|
|
k:bind({}, c, function() hints.processChar(c) end)
|
|
end
|
|
return k
|
|
end
|
|
|
|
--- hs.hints.windowHints([windows, callback, allowNonStandard])
|
|
--- Function
|
|
--- Displays a keyboard hint for switching focus to each window
|
|
---
|
|
--- Parameters:
|
|
--- * windows - An optional table containing some `hs.window` objects. If this value is nil, all windows will be hinted
|
|
--- * callback - An optional function that will be called when a window has been selected by the user. The function will be called with a single argument containing the `hs.window` object of the window chosen by the user
|
|
--- * allowNonStandard - An optional boolean. If true, all windows will be included, not just standard windows
|
|
---
|
|
--- Returns:
|
|
--- * None
|
|
---
|
|
--- Notes:
|
|
--- * If there are more windows open than there are characters available in hs.hints.hintChars, multiple characters will be used
|
|
--- * If hints.style is set to "vimperator", every window hint is prefixed with the first character of the parent application's name
|
|
--- * To display hints only for the currently focused application, try something like:
|
|
--- * `hs.hints.windowHints(hs.window.focusedWindow():application():allWindows())`
|
|
function hints.windowHints(windows, callback, allowNonStandard)
|
|
|
|
if hints.style == "vimperator" then
|
|
hintChars = hints.hintCharsVimperator
|
|
else
|
|
hintChars = hints.hintChars
|
|
end
|
|
|
|
windows = windows or window.allWindows()
|
|
selectionCallback = callback
|
|
|
|
if (modalKey == nil) then
|
|
modalKey = hints.setupModal()
|
|
end
|
|
hints.closeHints()
|
|
hintDict = {}
|
|
for _, win in ipairs(windows) do
|
|
local app = win:application()
|
|
if app and app:bundleID() and isValidWindow(win, allowNonStandard) then
|
|
if hints.style == "vimperator" then
|
|
local appchar = string.upper(string.sub(app:title(), 1, 1))
|
|
if hintDict[appchar] == nil then
|
|
hintDict[appchar] = {}
|
|
end
|
|
hints.addWindow(hintDict[appchar], win)
|
|
else
|
|
hints.addWindow(hintDict, win)
|
|
end
|
|
end
|
|
end
|
|
takenPositions = {}
|
|
if next(hintDict) ~= nil then
|
|
hints.displayHintsForDict(hintDict, "", nil, allowNonStandard)
|
|
modalKey:enter()
|
|
end
|
|
end
|
|
|
|
function hints.closeHints()
|
|
for _, hint in ipairs(openHints) do
|
|
hint:close()
|
|
end
|
|
openHints = {}
|
|
takenPositions = {}
|
|
end
|
|
|
|
return hints
|