315 lines
7.7 KiB
Lua
315 lines
7.7 KiB
Lua
local WindowHighlight = {}
|
|
WindowHighlight.__index = WindowHighlight
|
|
|
|
local function sameFrame(a, b)
|
|
if not a or not b then return false end
|
|
|
|
return math.abs(a.x - b.x) < 0.1
|
|
and math.abs(a.y - b.y) < 0.1
|
|
and math.abs(a.w - b.w) < 0.1
|
|
and math.abs(a.h - b.h) < 0.1
|
|
end
|
|
|
|
function WindowHighlight.new(config, callbacks)
|
|
local border = hs.drawing.rectangle(hs.geometry.rect(0, 0, 1, 1))
|
|
border.canvas[1] = {
|
|
type = "segments",
|
|
action = "stroke",
|
|
closed = true,
|
|
clipToPath = true,
|
|
coordinates = {
|
|
{ x = 0, y = 0 },
|
|
{ x = 1, y = 0 },
|
|
{ x = 1, y = 1 },
|
|
{ x = 0, y = 1 },
|
|
},
|
|
}
|
|
border:setFill(false)
|
|
border:setStroke(true)
|
|
border:setStrokeWidth(config.borderWidth)
|
|
border:setStrokeColor(config.color)
|
|
border:setLevel(hs.drawing.windowLevels.screenSaver)
|
|
border:setBehaviorByLabels({
|
|
"canJoinAllSpaces",
|
|
"stationary",
|
|
"ignoresCycle",
|
|
"fullScreenAuxiliary",
|
|
})
|
|
|
|
return setmetatable({
|
|
config = config,
|
|
callbacks = callbacks or {},
|
|
border = border,
|
|
borderRadius = config.radius + config.padding,
|
|
missionControlActive = false,
|
|
dockElement = nil,
|
|
liveWindowWatcher = nil,
|
|
watchedWindowId = nil,
|
|
lastPolledWindowId = nil,
|
|
lastPolledFrame = nil,
|
|
watchers = {},
|
|
}, WindowHighlight)
|
|
end
|
|
|
|
function WindowHighlight:isMissionControlActive()
|
|
return self.missionControlActive
|
|
end
|
|
|
|
function WindowHighlight:rectForWindow(win)
|
|
local frame = win:frame()
|
|
local screen = win:screen()
|
|
local screenFrame = screen:fullFrame()
|
|
local visibleFrame = screen:frame()
|
|
local pad = self.config.padding
|
|
local left = frame.x - pad
|
|
local top = frame.y - pad
|
|
local right = frame.x + frame.w + pad
|
|
local bottom = frame.y + frame.h + pad
|
|
local touchesScreen = left <= screenFrame.x
|
|
or top <= visibleFrame.y
|
|
or right >= screenFrame.x + screenFrame.w
|
|
or bottom >= screenFrame.y + screenFrame.h
|
|
|
|
if touchesScreen then return frame, self.config.radius end
|
|
|
|
return {
|
|
x = left,
|
|
y = top,
|
|
w = right - left,
|
|
h = bottom - top,
|
|
}, self.config.radius + pad
|
|
end
|
|
|
|
function WindowHighlight:isValidWindow(win)
|
|
if not win then return false end
|
|
if not win:isStandard() then return false end
|
|
if self.config.hideOnFullscreen and win:isFullScreen() then return false end
|
|
|
|
return win:application() ~= nil
|
|
end
|
|
|
|
function WindowHighlight:hide()
|
|
self.border:hide()
|
|
end
|
|
|
|
function WindowHighlight:isMissionControlOpen()
|
|
if not (self.dockElement and self.dockElement:isValid()) then
|
|
local dockApps = hs.application.applicationsForBundleID("com.apple.dock")
|
|
local dockApp = dockApps and dockApps[1]
|
|
|
|
if not dockApp then return false end
|
|
self.dockElement = hs.axuielement.applicationElement(dockApp)
|
|
end
|
|
|
|
for _, element in ipairs(self.dockElement) do
|
|
if element.AXIdentifier == "mc" then return true end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function WindowHighlight:setFrame(frame)
|
|
self.border:setFrame(frame)
|
|
self:updateBorderPath(frame)
|
|
self.border:show()
|
|
end
|
|
|
|
function WindowHighlight:updateBorderPath(frame)
|
|
local width = frame.w
|
|
local height = frame.h
|
|
local radius = math.min(self.borderRadius, width / 2, height / 2)
|
|
local bezier = 0.5522847498
|
|
|
|
self.border.canvas[1].coordinates = {
|
|
{ x = radius, y = 0 },
|
|
{ x = width - radius, y = 0 },
|
|
{
|
|
x = width,
|
|
y = radius,
|
|
c1x = width - radius + bezier * radius,
|
|
c1y = 0,
|
|
c2x = width,
|
|
c2y = radius - bezier * radius,
|
|
},
|
|
{ x = width, y = height - radius },
|
|
{
|
|
x = width - radius,
|
|
y = height,
|
|
c1x = width,
|
|
c1y = height - radius + bezier * radius,
|
|
c2x = width - radius + bezier * radius,
|
|
c2y = height,
|
|
},
|
|
{ x = radius, y = height },
|
|
{
|
|
x = 0,
|
|
y = height - radius,
|
|
c1x = radius - bezier * radius,
|
|
c1y = height,
|
|
c2x = 0,
|
|
c2y = height - radius + bezier * radius,
|
|
},
|
|
{ x = 0, y = radius },
|
|
{
|
|
x = radius,
|
|
y = 0,
|
|
c1x = 0,
|
|
c1y = radius - bezier * radius,
|
|
c2x = radius - bezier * radius,
|
|
c2y = 0,
|
|
},
|
|
}
|
|
end
|
|
|
|
function WindowHighlight:update()
|
|
if self.missionControlActive then
|
|
self:hide()
|
|
return
|
|
end
|
|
|
|
local win = hs.window.focusedWindow()
|
|
if not self:isValidWindow(win) then
|
|
self:hide()
|
|
return
|
|
end
|
|
|
|
local targetFrame, targetRadius = self:rectForWindow(win)
|
|
self.borderRadius = targetRadius
|
|
self:setFrame(targetFrame)
|
|
end
|
|
|
|
function WindowHighlight:watchFocusedWindowLive()
|
|
local win = hs.window.focusedWindow()
|
|
local windowId = win and win:id() or nil
|
|
|
|
if self.liveWindowWatcher and self.watchedWindowId == windowId then return end
|
|
|
|
if self.liveWindowWatcher then
|
|
self.liveWindowWatcher:stop()
|
|
self.liveWindowWatcher = nil
|
|
end
|
|
|
|
self.watchedWindowId = windowId
|
|
if not win then return end
|
|
|
|
self.liveWindowWatcher = win:newWatcher(function(_, event)
|
|
if event == hs.uielement.watcher.elementDestroyed then
|
|
if self.liveWindowWatcher then
|
|
self.liveWindowWatcher:stop()
|
|
self.liveWindowWatcher = nil
|
|
end
|
|
|
|
self.watchedWindowId = nil
|
|
self:hide()
|
|
|
|
hs.timer.doAfter(0.05, function()
|
|
self:watchFocusedWindowLive()
|
|
self:update()
|
|
end)
|
|
elseif event == hs.uielement.watcher.windowMoved
|
|
or event == hs.uielement.watcher.windowResized then
|
|
self:update()
|
|
end
|
|
end)
|
|
|
|
self.liveWindowWatcher:start({
|
|
hs.uielement.watcher.windowMoved,
|
|
hs.uielement.watcher.windowResized,
|
|
hs.uielement.watcher.elementDestroyed,
|
|
})
|
|
end
|
|
|
|
function WindowHighlight:start()
|
|
local windowFilter = hs.window.filter.new(nil)
|
|
windowFilter:subscribe({
|
|
hs.window.filter.windowFocused,
|
|
hs.window.filter.windowMoved,
|
|
hs.window.filter.windowTitleChanged,
|
|
hs.window.filter.windowUnfocused,
|
|
}, function()
|
|
self:update()
|
|
end)
|
|
|
|
windowFilter:subscribe(hs.window.filter.windowFocused, function()
|
|
self:watchFocusedWindowLive()
|
|
if self.callbacks.onWindowFocused then
|
|
self.callbacks.onWindowFocused()
|
|
end
|
|
end)
|
|
|
|
local applicationWatcher = hs.application.watcher.new(function()
|
|
hs.timer.doAfter(0.05, function()
|
|
self:watchFocusedWindowLive()
|
|
self:update()
|
|
end)
|
|
end)
|
|
applicationWatcher:start()
|
|
|
|
local spacesWatcher = hs.spaces.watcher.new(function()
|
|
hs.timer.doAfter(0.15, function()
|
|
self:update()
|
|
end)
|
|
end)
|
|
spacesWatcher:start()
|
|
|
|
local missionControlWatcher = hs.timer.doEvery(0.1, function()
|
|
local ok, isOpen = pcall(function()
|
|
return self:isMissionControlOpen()
|
|
end)
|
|
if not ok or isOpen == self.missionControlActive then return end
|
|
|
|
self.missionControlActive = isOpen
|
|
|
|
if self.missionControlActive then
|
|
self:hide()
|
|
else
|
|
hs.timer.doAfter(0.15, function()
|
|
self:update()
|
|
end)
|
|
end
|
|
end)
|
|
|
|
local framePollingWatcher = hs.timer.doEvery(
|
|
1 / self.config.framePollingFps,
|
|
function()
|
|
if self.missionControlActive then return end
|
|
|
|
local win = hs.window.focusedWindow()
|
|
local windowId = win and win:id() or nil
|
|
|
|
if windowId ~= self.lastPolledWindowId then
|
|
self.lastPolledWindowId = windowId
|
|
self.lastPolledFrame = win and win:frame() or nil
|
|
self:watchFocusedWindowLive()
|
|
self:update()
|
|
return
|
|
end
|
|
|
|
if not win then return end
|
|
|
|
local frame = win:frame()
|
|
if sameFrame(frame, self.lastPolledFrame) then return end
|
|
|
|
self.lastPolledFrame = frame
|
|
self:update()
|
|
end
|
|
)
|
|
|
|
self.watchers = {
|
|
windowFilter = windowFilter,
|
|
application = applicationWatcher,
|
|
spaces = spacesWatcher,
|
|
missionControl = missionControlWatcher,
|
|
framePolling = framePollingWatcher,
|
|
}
|
|
|
|
hs.timer.doAfter(0.5, function()
|
|
self:update()
|
|
self:watchFocusedWindowLive()
|
|
end)
|
|
|
|
return self
|
|
end
|
|
|
|
return WindowHighlight
|