316 lines
15 KiB
Lua
316 lines
15 KiB
Lua
local module = {}
|
|
|
|
--- === hs.alert ===
|
|
---
|
|
--- Simple on-screen alerts
|
|
|
|
local drawing = require("hs.drawing")
|
|
local timer = require("hs.timer")
|
|
local screen = require("hs.screen")
|
|
local uuid = require"hs.host".uuid
|
|
local stext = require"hs.styledtext".new
|
|
|
|
local stextMT = hs.getObjectMetatable("hs.styledtext")
|
|
|
|
module._visibleAlerts = {}
|
|
|
|
--- hs.alert.defaultStyle[]
|
|
--- Variable
|
|
--- A table defining the default visual style for the alerts generated by this module.
|
|
---
|
|
--- The following may be specified in this table (any other key is ignored):
|
|
--- * Keys which affect the alert rectangle:
|
|
--- * fillColor - a table as defined by the `hs.drawing.color` module to specify the background color for the alert, defaults to { white = 0, alpha = 0.75 }.
|
|
--- * strokeColor - a table as defined by the `hs.drawing.color` module to specify the outline color for the alert, defaults to { white = 1, alpha = 1 }.
|
|
--- * strokeWidth - a number specifying the width of the outline for the alert, defaults to 2
|
|
--- * radius - a number specifying the radius used for the rounded corners of the alert box, defaults to 27
|
|
---
|
|
--- * Keys which affect the text of the alert when the message is a string (note that these keys will be ignored if the message being displayed is already an `hs.styledtext` object):
|
|
--- * textColor - a table as defined by the `hs.drawing.color` module to specify the message text color for the alert, defaults to { white = 1, alpha = 1 }.
|
|
--- * textFont - a string specifying the font to be used for the alert text, defaults to ".AppleSystemUIFont" which is a symbolic name representing the systems default user interface font.
|
|
--- * textSize - a number specifying the font size to be used for the alert text, defaults to 27.
|
|
--- * textStyle - an optional table, defaults to `nil`, specifying that a string message should be converted to an `hs.styledtext` object using the style elements specified in this table. This table should conform to the key-value pairs as described in the documentation for the `hs.styledtext` module. If this table does not contain a `font` key-value pair, one will be constructed from the `textFont` and `textSize` keys (or their defaults); likewise, if this table does not contain a `color` key-value pair, one will be constructed from the `textColor` key (or its default).
|
|
--- * padding - the number of pixels to reserve around each side of the text and/or image, defaults to textSize/2
|
|
--- * atScreenEdge - 0: screen center (default); 1: top edge; 2: bottom edge . Note when atScreenEdge>0, the latest alert will overlay above the previous ones if multiple alerts visible on same edge; and when atScreenEdge=0, latest alert will show below previous visible ones without overlap.
|
|
--- * fadeInDuration - a number in seconds specifying the fade in duration of the alert, defaults to 0.15
|
|
--- * fadeOutDuration - a number in seconds specifying the fade out duration of the alert, defaults to 0.15
|
|
---
|
|
--- If you modify these values directly, it will affect all future alerts generated by this module. To adjust one of these properties for a single alert, use the optional `style` argument to the [hs.alert.show](#show) function.
|
|
module.defaultStyle = {
|
|
strokeWidth = 2,
|
|
strokeColor = { white = 1, alpha = 1 },
|
|
fillColor = { white = 0, alpha = 0.75 },
|
|
textColor = { white = 1, alpha = 1 },
|
|
textFont = ".AppleSystemUIFont",
|
|
textSize = 27,
|
|
radius = 27,
|
|
atScreenEdge = 0,
|
|
fadeInDuration = 0.15,
|
|
fadeOutDuration = 0.15,
|
|
padding = nil,
|
|
}
|
|
|
|
local purgeAlert = function(UUID, duration)
|
|
duration = math.max(duration, 0.0) or 0.15
|
|
local indexToRemove
|
|
for i,v in ipairs(module._visibleAlerts) do
|
|
if v.UUID == UUID then
|
|
if v.timer then v.timer:stop() end
|
|
for i2,v2 in ipairs(v.drawings) do
|
|
v2:hide(duration)
|
|
if duration > 0.0 then
|
|
timer.doAfter(duration, function() v2:delete() end)
|
|
end
|
|
v.drawings[i2] = nil
|
|
end
|
|
indexToRemove = i
|
|
break
|
|
end
|
|
end
|
|
if indexToRemove then
|
|
table.remove(module._visibleAlerts, indexToRemove)
|
|
end
|
|
end
|
|
|
|
local showAlert = function(message, image, style, screenObj, duration)
|
|
local thisAlertStyle = {}
|
|
for k,v in pairs(module.defaultStyle) do thisAlertStyle[k] = v end
|
|
if type(style) == "table" then
|
|
for k,v in pairs(style) do thisAlertStyle[k] = v end
|
|
end
|
|
|
|
local textSize = thisAlertStyle.textSize
|
|
local textFont = thisAlertStyle.textFont
|
|
local textColor = thisAlertStyle.textColor
|
|
|
|
if type(thisAlertStyle.textStyle) == "table" and getmetatable(message) ~= stextMT then
|
|
if not thisAlertStyle.textStyle.font then
|
|
thisAlertStyle.textStyle.font = { name = textFont, size = textSize }
|
|
end
|
|
if not thisAlertStyle.textStyle.color then
|
|
thisAlertStyle.textStyle.color = textColor
|
|
end
|
|
textSize = thisAlertStyle.textStyle.font.size
|
|
textFont = thisAlertStyle.textStyle.font.name
|
|
textColor = thisAlertStyle.textStyle.color
|
|
message = stext(message, thisAlertStyle.textStyle)
|
|
-- print(finspect(message:asTable()))
|
|
end
|
|
|
|
local screenFrame = screenObj:fullFrame()
|
|
|
|
local absoluteTop = screenFrame.y + (screenFrame.h * (1 - 1 / 1.55) + 55) -- mimic module behavior for inverted rect
|
|
if thisAlertStyle.atScreenEdge > 0 then
|
|
absoluteTop = screenFrame.y -- this is for atScreenEdge = 1, and atScreenEdge = 2 case will be handled later
|
|
else
|
|
if #module._visibleAlerts > 0 then
|
|
-- we're looking for the latest on the same screen
|
|
for i = #module._visibleAlerts, 1, -1 do
|
|
if screenObj == module._visibleAlerts[i].screen and module._visibleAlerts[i].atScreenEdge == 0 then
|
|
absoluteTop = module._visibleAlerts[i].frame.y + module._visibleAlerts[i].frame.h + 3
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if absoluteTop > (screenFrame.y + screenFrame.h) then
|
|
absoluteTop = screenFrame.y
|
|
end
|
|
|
|
local alertEntry = {
|
|
drawings = {},
|
|
screen = screenObj,
|
|
atScreenEdge = thisAlertStyle.atScreenEdge
|
|
}
|
|
local UUID = uuid()
|
|
alertEntry.UUID = UUID
|
|
|
|
local padding = thisAlertStyle.padding or textSize/2
|
|
local strokeWidth = thisAlertStyle.strokeWidth -- strokeWidth should be used to adjust position and padding.
|
|
|
|
-- If no message is specified, don't reserve space for it
|
|
local textFrame
|
|
if message == "" then
|
|
textFrame = {h = 0, w = 0}
|
|
else
|
|
textFrame = drawing.getTextDrawingSize(message, { font = textFont, size = textSize })
|
|
textFrame.w = math.ceil(textFrame.w) -- drawing.getTextDrawingSize may return a float value, and use it directly could cause some display problem, the last character of a line may disappear.
|
|
end
|
|
|
|
-- Define the size of the drawing frame
|
|
local drawingFrame = {
|
|
h = textFrame.h + padding * 2 + strokeWidth,
|
|
w = textFrame.w + padding * 2 + strokeWidth,
|
|
}
|
|
if image then
|
|
-- Increase the size to make room for the image
|
|
drawingFrame.w = drawingFrame.w + image:size().w
|
|
drawingFrame.h = math.max(drawingFrame.h, image:size().h + padding * 2 + strokeWidth)
|
|
if message ~= "" then
|
|
--Add space for padding between the image and the message
|
|
drawingFrame.w = drawingFrame.w + padding
|
|
end
|
|
end
|
|
|
|
-- Use the size to set the position
|
|
drawingFrame.x = screenFrame.x + (screenFrame.w - drawingFrame.w) / 2
|
|
|
|
if thisAlertStyle.atScreenEdge == 2 then
|
|
drawingFrame.y = screenFrame.y + screenFrame.h - drawingFrame.h
|
|
else
|
|
drawingFrame.y = absoluteTop
|
|
end
|
|
|
|
table.insert(alertEntry.drawings, drawing.rectangle(drawingFrame)
|
|
:setStroke(true)
|
|
:setStrokeWidth(thisAlertStyle.strokeWidth)
|
|
:setStrokeColor(thisAlertStyle.strokeColor)
|
|
:setFill(true)
|
|
:setFillColor(thisAlertStyle.fillColor)
|
|
:setRoundedRectRadii(thisAlertStyle.radius, thisAlertStyle.radius)
|
|
:show(thisAlertStyle.fadeInDuration)
|
|
)
|
|
|
|
-- Constraints for placing the text
|
|
local textMinX = drawingFrame.x
|
|
local textMaxWidth = drawingFrame.w
|
|
|
|
if image then
|
|
local iconFrame = {
|
|
x = drawingFrame.x + padding + strokeWidth / 2,
|
|
y = drawingFrame.y + (drawingFrame.h - image:size().h) / 2,
|
|
h = image:size().h,
|
|
w = image:size().w
|
|
}
|
|
table.insert(alertEntry.drawings, drawing.image(iconFrame, image)
|
|
:orderAbove(alertEntry.drawings[1])
|
|
:show(thisAlertStyle.fadeInDuration)
|
|
)
|
|
|
|
-- Shrink the space the text can draw in to account for the image
|
|
textMinX = textMinX + iconFrame.w + padding
|
|
textMaxWidth = textMaxWidth - iconFrame.w - padding
|
|
end
|
|
|
|
-- Draw the text in the center of the remaining space
|
|
textFrame.x = textMinX + (textMaxWidth - textFrame.w) / 2
|
|
textFrame.y = drawingFrame.y + (drawingFrame.h - textFrame.h) / 2
|
|
table.insert(alertEntry.drawings, drawing.text(textFrame, message)
|
|
:setTextFont(textFont)
|
|
:setTextSize(textSize)
|
|
:setTextColor(textColor)
|
|
:orderAbove(alertEntry.drawings[1])
|
|
:show(thisAlertStyle.fadeInDuration)
|
|
)
|
|
alertEntry.frame = drawingFrame
|
|
|
|
table.insert(module._visibleAlerts, alertEntry)
|
|
if type(duration) == "number" then
|
|
alertEntry.timer = timer.doAfter(duration, function()
|
|
purgeAlert(UUID, thisAlertStyle.fadeOutDuration)
|
|
end)
|
|
end
|
|
return UUID
|
|
end
|
|
|
|
--- hs.alert.showWithImage(str, image, [style], [screen], [seconds]) -> uuid
|
|
--- Function
|
|
--- Shows an image and a message in large words briefly in the middle of the screen; does tostring() on its argument for convenience.
|
|
---
|
|
--- Parameters:
|
|
--- * str - The string or `hs.styledtext` object to display in the alert
|
|
--- * image - The image to display in the alert
|
|
--- * style - an optional table containing one or more of the keys specified in [hs.alert.defaultStyle](#defaultStyle). If `str` is already an `hs.styledtext` object, this argument is ignored.
|
|
--- * screen - an optional `hs.screen` userdata object specifying the screen (monitor) to display the alert on. Defaults to `hs.screen.mainScreen()` which corresponds to the screen with the currently focused window.
|
|
--- * seconds - The number of seconds to display the alert. Defaults to 2. If seconds is specified and is not a number, displays the alert indefinately.
|
|
---
|
|
--- Returns:
|
|
--- * a string identifier for the alert.
|
|
---
|
|
--- Notes:
|
|
--- * The optional parameters are parsed in the order presented as follows:
|
|
--- * if the argument is a table and `style` has not previously been set, then the table is assigned to `style`
|
|
--- * if the argument is a userdata and `screen` has not previously been set, then the userdata is assigned to `screen`
|
|
--- * if `duration` has not been set, then it is assigned the value of the argument
|
|
--- * if all of these conditions fail for a given argument, then an error is returned
|
|
--- * The reason for this logic is to support the creation of persistent alerts as was previously handled by the module: If you specify a non-number value for `seconds` you will need to store the string identifier returned by this function so that you can close it manually with `hs.alert.closeSpecific` when the alert should be removed.
|
|
--- * Any style element which is not specified in the `style` argument table will use the value currently defined in the [hs.alert.defaultStyle](#defaultStyle) table.
|
|
module.showWithImage = function(message, image, ...)
|
|
local style, screenObj, duration
|
|
for i,v in ipairs(table.pack(...)) do
|
|
if type(v) == "table" and not style then
|
|
style = v
|
|
elseif type(v) == "userdata" and not screenObj then
|
|
screenObj = v
|
|
elseif type(duration) == "nil" then
|
|
duration = v
|
|
else
|
|
error("unexpected type " .. type(v) .. " found for argument " .. tostring(i + 1), 2)
|
|
end
|
|
end
|
|
if getmetatable(message) ~= stextMT then
|
|
message = tostring(message)
|
|
end
|
|
duration = duration or 2.0
|
|
screenObj = screenObj or screen.mainScreen()
|
|
return showAlert(message, image, style, screenObj, duration)
|
|
end
|
|
|
|
--- hs.alert.show(str, [style], [screen], [seconds]) -> uuid
|
|
--- Function
|
|
--- Shows a message in large words briefly in the middle of the screen; does tostring() on its argument for convenience.
|
|
---
|
|
--- Parameters:
|
|
--- * str - The string or `hs.styledtext` object to display in the alert
|
|
--- * style - an optional table containing one or more of the keys specified in [hs.alert.defaultStyle](#defaultStyle). If `str` is already an `hs.styledtext` object, this argument is ignored.
|
|
--- * screen - an optional `hs.screen` userdata object specifying the screen (monitor) to display the alert on. Defaults to `hs.screen.mainScreen()` which corresponds to the screen with the currently focused window.
|
|
--- * seconds - The number of seconds to display the alert. Defaults to 2. If seconds is specified and is not a number, displays the alert indefinately.
|
|
---
|
|
--- Returns:
|
|
--- * a string identifier for the alert.
|
|
---
|
|
--- Notes:
|
|
--- * For convenience, you can call this function as `hs.alert(...)`
|
|
--- * This function effectively calls `hs.alert.showWithImage(msg, nil, ...)`. As such, all the same rules apply regarding argument processing
|
|
module.show = function(message, ...)
|
|
return module.showWithImage(message, nil, ...)
|
|
end
|
|
|
|
|
|
--- hs.alert.closeAll([seconds])
|
|
--- Function
|
|
--- Closes all alerts currently open on the screen
|
|
---
|
|
--- Parameters:
|
|
--- * seconds - Optional number specifying the fade out duration. Defaults to `fadeOutDuration` value currently defined in the [hs.alert.defaultStyle](#defaultStyle)
|
|
---
|
|
--- Returns:
|
|
--- * None
|
|
module.closeAll = function(duration)
|
|
duration = duration and math.max(duration, 0.0) or module.defaultStyle.fadeOutDuration
|
|
while (#module._visibleAlerts > 0) do
|
|
purgeAlert(module._visibleAlerts[#module._visibleAlerts].UUID, duration)
|
|
end
|
|
end
|
|
|
|
--- hs.alert.closeSpecific(uuid, [seconds])
|
|
--- Function
|
|
--- Closes the alert with the specified identifier
|
|
---
|
|
--- Parameters:
|
|
--- * uuid - the identifier of the alert to close
|
|
--- * seconds - Optional number specifying the fade out duration. Defaults to `fadeOutDuration` value currently defined in the [hs.alert.defaultStyle](#defaultStyle)
|
|
---
|
|
--- Returns:
|
|
--- * None
|
|
---
|
|
--- Notes:
|
|
--- * Use this function to close an alert which is indefinate or close an alert with a long duration early.
|
|
module.closeSpecific = function(UUID, duration)
|
|
duration = duration and math.max(duration, 0.0) or module.defaultStyle.fadeOutDuration
|
|
purgeAlert(UUID, duration)
|
|
end
|
|
|
|
return setmetatable(module, { __call = function(_, ...) return module.show(...) end })
|