feat: show notification badges on top of app icons (closes #523)

This commit is contained in:
Louis Pontoise authored and lwouis committed 2020-08-18 23:35:13 +09:00
1 parent d3253ba4e1
commit fb628341d1
32 files changed
+141 -36

No files matched your search

Binary file not shown.
+3
View File
@@ -127,6 +127,9 @@
/* No comment provided by engineer. */
"Hidden" = "Hidden";
/* No comment provided by engineer. */
"Hide app badges:" = "Hide app badges:";
/* No comment provided by engineer. */
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "مخفي";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Versteckt";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Bunte Kreise beim Markieren mit Maus verbergen:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Hidden";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Ocultas";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Piilossa";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Cachées";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Rejtett";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Tersembunyi";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Nascosto";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "ウィンドウをしまう";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "숨겨진 화면";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "마우스를 올릴 때 색칠된 원들 숨기기:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Verstoppen";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Verborgen";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Escondido";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Esconder círculos coloridos onde o rato passar:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Скрытые";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Скрыть цветные круги при наведении мышью:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "Gizli";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "隐藏";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "鼠标悬停时隐藏彩色圆圈:";
@@ -128,6 +128,9 @@
/*No comment provided by engineer.*/
"Hidden" = "隱藏";
/*No comment provided by engineer.*/
"Hide app badges:" = "Hide app badges:";
/*No comment provided by engineer.*/
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
+1 -1
View File
@@ -4,4 +4,4 @@ set -exu
"$(pipenv --venv)"/bin/pyftsubset resources/SF-Pro-Text-Regular-Full.otf \
--output-file=resources/SF-Pro-Text-Regular.otf \
--text="􀀁􀁑􀁏􀁍􀁎􀁌􀕧􀕬􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀘠􀚗􀚙􀚛􀚝􀚟􀚡􀚣􀚥􀚧􀚩􀚫􀚭􀚯􀚱􀚳􀚵􀚷􀚹􀚻"
--text="􀀁􀁑􀁏􀁍􀁎􀁌􀕧􀕬􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀀹􀀻􀀽􀀿􀁁􀘘􀁃􀁅􀑲􀁇􀁉􀁋􀑴􀔔􀔕􀔖􀔗􀔘􀔙􀔚􀔛􀔜􀔝􀔞􀔟􀔠􀔡􀔢􀔣􀔤􀔥􀔦􀔧􀔨􀕭"
+8
View File
@@ -116,6 +116,10 @@ extension AXUIElement {
return try attribute(kAXParentAttribute, AXUIElement.self)
}
func children() throws -> [AXUIElement]? {
return try attribute(kAXChildrenAttribute, [AXUIElement].self)
}
func windows() throws -> [AXUIElement]? {
return try attribute(kAXWindowsAttribute, [AXUIElement].self)
}
@@ -140,6 +144,10 @@ extension AXUIElement {
return try attribute(kAXSubroleAttribute, String.self)
}
func appIsRunning() throws -> Bool? {
return try attribute(kAXIsApplicationRunningAttribute, Bool.self)
}
func closeButton() throws -> AXUIElement? {
return try attribute(kAXCloseButtonAttribute, AXUIElement.self)
}
+2 -1
View File
@@ -121,8 +121,9 @@ func CGSGetWindowLevel(_ cid: CGSConnectionID, _ wid: CGWindowID, _ level: inout
@_silgen_name("SLSRequestScreenCaptureAccess") @discardableResult
func SLSRequestScreenCaptureAccess() -> UInt8
// for some reason, this attribute is missing from AXAttributeConstants
// for some reason, these attributes are missing from AXAttributeConstants
let kAXFullscreenAttribute = "AXFullScreen"
let kAXStatusLabelAttribute = "AXStatusLabel"
// enables/disables a symbolic hotkeys. These are system shortcuts such as command+tab or Spotlight
// it is possible to find all the existing hotkey IDs by using CGSGetSymbolicHotKeyValue on the first few hundred numbers
+1
View File
@@ -7,6 +7,7 @@ class Application: NSObject {
var axUiElement: AXUIElement?
var axObserver: AXObserver?
var isReallyFinishedLaunching = false
var dockLabel: String?
static func notifications(_ app: NSRunningApplication) -> [String] {
var n = [
+15
View File
@@ -73,6 +73,21 @@ class Applications {
App.app.refreshOpenUi()
}
static func refreshBadges() {
retryAxCallUntilTimeout {
let axDock = AXUIElementCreateApplication(Applications.list.first { $0.runningApplication.bundleIdentifier == "com.apple.dock" }!.runningApplication.processIdentifier)
let axList = try axDock.children()!.first { try $0.role() == "AXList" }!
let axAppDockItem = try axList.children()!.filter { try $0.subrole() == "AXApplicationDockItem" && ($0.appIsRunning() ?? false) }
try Applications.list.forEach { app in
if app.runningApplication.activationPolicy == .regular,
let bundleId = app.runningApplication.bundleIdentifier,
let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
app.dockLabel = try axAppDockItem.first { try $0.attribute(kAXURLAttribute, URL.self) == url }?.attribute(kAXStatusLabelAttribute, String.self)
}
}
}
}
private static func isActualApplication(_ app: NSRunningApplication) -> Bool {
return (app.activationPolicy != .prohibited || isNotXpc(app)) && notAltTab(app)
}
+2
View File
@@ -58,6 +58,7 @@ class Preferences {
"minCellsPerRow": defaultsDependingOnScreenRatio_["minCellsPerRow"]!,
"maxCellsPerRow": defaultsDependingOnScreenRatio_["maxCellsPerRow"]!,
"shortcutStyle": "0",
"hideAppBadges": "false",
]
// constant values
@@ -96,6 +97,7 @@ class Preferences {
static var fadeOutAnimation: Bool { defaults.bool("fadeOutAnimation") }
static var hideSpaceNumberLabels: Bool { defaults.bool("hideSpaceNumberLabels") }
static var hideStatusIcons: Bool { defaults.bool("hideStatusIcons") }
static var hideAppBadges: Bool { defaults.bool("hideAppBadges") }
static var startAtLogin: Bool { defaults.bool("startAtLogin") }
static var dontShowBlacklist: [String] { blacklistStringToArray(defaults.string("dontShowBlacklist")) }
static var disableShortcutsBlacklist: [String] { blacklistStringToArray(defaults.string("disableShortcutsBlacklist")) }
+3 -4
View File
@@ -4,10 +4,11 @@ class Window {
var cgWindowId: CGWindowID
var title: String!
var thumbnail: NSImage?
var icon: NSImage?
var icon: NSImage? { get { application.runningApplication.icon } }
var shouldShowTheUser = true
var isTabbed: Bool = false
var isHidden: Bool
var isHidden: Bool { get { application.runningApplication.isHidden } }
var dockLabel: Int? { get { application.dockLabel.flatMap { Int($0) } } }
var isFullscreen: Bool
var isMinimized: Bool
var isOnAllSpaces: Bool
@@ -35,8 +36,6 @@ class Window {
self.cgWindowId = wid
self.spaceId = Spaces.currentSpaceId
self.spaceIndex = Spaces.currentSpaceIndex
self.icon = application.runningApplication.icon
self.isHidden = application.runningApplication.isHidden
self.isFullscreen = isFullscreen
self.isMinimized = isMinimized
self.isOnAllSpaces = false
+1 -5
View File
@@ -86,11 +86,7 @@ private func applicationHiddenOrShown(_ element: AXUIElement, _ pid: pid_t, _ ty
DispatchQueue.main.async {
let windows = Windows.list.filter {
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
let isFromApp = $0.application.runningApplication.processIdentifier == pid
if isFromApp {
$0.isHidden = type == kAXApplicationHiddenNotification
}
return isFromApp
return $0.application.runningApplication.processIdentifier == pid
}
App.app.refreshOpenUi(windows)
}
+2
View File
@@ -270,6 +270,8 @@ class App: AppCenterApplication, NSApplicationDelegate {
guard appIsBeingUsed else { return }
Windows.refreshAllThumbnails()
guard appIsBeingUsed else { return }
Applications.refreshBadges()
guard appIsBeingUsed else { return }
refreshOpenUi()
guard appIsBeingUsed else { return }
thumbnailsPanel.show()
+25 -19
View File
@@ -7,22 +7,21 @@ enum Symbols: String {
case circledNumber0 = "􀀸"
case circledNumber10 = "􀓵"
case circledStar = "􀕬"
case filledCircledStar = "􀕭"
case filledCircled = "􀀁"
case filledCircledMultiplySign = "􀁑"
case filledCircledMinusSign = "􀁏"
case filledCircledPlusSign = "􀁍"
case filledCircledNumber0 = "􀀹"
case filledCircledNumber10 = "􀔔"
}
// Font icon using SF Symbols from the SF Pro font from Apple
// see https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
class ThumbnailFontIconView: ThumbnailTitleView {
convenience init(_ symbol: Symbols, _ size: CGFloat = Preferences.fontIconSize, _ color: NSColor = .white, _ isBackground: Bool = false) {
convenience init(_ symbol: Symbols, _ size: CGFloat = Preferences.fontIconSize, _ color: NSColor = .white, _ shadow: NSShadow? = ThumbnailView.makeShadow(.darkGray)) {
// This helps SF symbols display vertically centered and not clipped at the bottom
if isBackground {
self.init(size, 3, shadow: nil)
} else {
self.init(size, 3)
}
self.init(size, 3, shadow: shadow)
string = symbol.rawValue
font = NSFont(name: "SF Pro Text", size: size)
textColor = color
@@ -31,32 +30,39 @@ class ThumbnailFontIconView: ThumbnailTitleView {
}
// number should be in the interval [0-50]
func setNumber(_ number: UInt32) {
let (baseCharacter, offset) = baseCharacterAndOffset(number)
assignIfDifferent(&string, String(UnicodeScalar(baseCharacter.unicodeScalars.first!.value + offset)!))
func setNumber(_ number: Int, _ filled: Bool) {
let (baseCharacter, offset) = baseCharacterAndOffset(number, filled)
assignIfDifferent(&string, String(UnicodeScalar(Int(baseCharacter.unicodeScalars.first!.value) + offset)!))
}
private func baseCharacterAndOffset(_ number: Int, _ filled: Bool) -> (String, Int) {
if number <= 9 {
// numbers alternate between empty and full circles; we skip the full circles
return ((filled ? Symbols.filledCircledNumber0 : Symbols.circledNumber0).rawValue, number * 2)
} else {
return ((filled ? Symbols.filledCircledNumber10 : Symbols.circledNumber10).rawValue, number - 10)
}
}
func setStar() {
assignIfDifferent(&string, Symbols.circledStar.rawValue)
}
private func baseCharacterAndOffset(_ number: UInt32) -> (String, UInt32) {
if number <= 9 {
// numbers alternate between empty and full circles; we skip the full circles
return (Symbols.circledNumber0.rawValue, number * UInt32(2))
} else {
return (Symbols.circledNumber10.rawValue, number - 10)
}
func setFilledStar() {
assignIfDifferent(&string, Symbols.filledCircledStar.rawValue)
}
}
class ThumbnailFilledFontIconView: NSView {
convenience init(_ thumbnailFontIconView: ThumbnailFontIconView, _ backgroundColor: NSColor) {
convenience init(_ thumbnailFontIconView: ThumbnailFontIconView, _ backgroundColor: NSColor, _ offset: Bool = false) {
self.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
let backgroundView = ThumbnailFontIconView(.filledCircled, thumbnailFontIconView.font!.pointSize, backgroundColor, true)
let backgroundView = ThumbnailFontIconView(.filledCircled, thumbnailFontIconView.font!.pointSize - (offset ? 2 : 0), backgroundColor)
addSubview(backgroundView)
addSubview(thumbnailFontIconView, positioned: .above, relativeTo: nil)
fit(backgroundView.fittingSize.width, backgroundView.fittingSize.height)
if offset {
backgroundView.leftAnchor.constraint(equalTo: backgroundView.superview!.leftAnchor, constant: 1).isActive = true
}
fit(thumbnailFontIconView.fittingSize.width, thumbnailFontIconView.fittingSize.height)
}
}
+23 -6
View File
@@ -9,9 +9,10 @@ class ThumbnailView: NSStackView {
var minimizedIcon = ThumbnailFontIconView(.circledMinusSign)
var hiddenIcon = ThumbnailFontIconView(.circledSlashSign)
var spaceIcon = ThumbnailFontIconView(.circledNumber0)
var closeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledMultiplySign, Preferences.fontIconSize, NSColor(srgbRed: 1, green: 0.35, blue: 0.32, alpha: 1)), NSColor(srgbRed: 0.64, green: 0.03, blue: 0.02, alpha: 1))
var minimizeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledMinusSign, Preferences.fontIconSize, NSColor(srgbRed: 0.91, green: 0.75, blue: 0.16, alpha: 1)), NSColor(srgbRed: 0.71, green: 0.55, blue: 0.09, alpha: 1))
var maximizeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledPlusSign, Preferences.fontIconSize, NSColor(srgbRed: 0.32, green: 0.76, blue: 0.17, alpha: 1)), NSColor(srgbRed: 0.04, green: 0.39, blue: 0.02, alpha: 1))
var dockLabelIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledNumber0, (Preferences.fontIconSize * 0.7).rounded(), NSColor(srgbRed: 1, green: 0.30, blue: 0.25, alpha: 1), nil), NSColor.white, true)
var closeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledMultiplySign, Preferences.fontIconSize, NSColor(srgbRed: 1, green: 0.35, blue: 0.32, alpha: 1), nil), NSColor(srgbRed: 0.64, green: 0.03, blue: 0.02, alpha: 1))
var minimizeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledMinusSign, Preferences.fontIconSize, NSColor(srgbRed: 0.91, green: 0.75, blue: 0.16, alpha: 1), nil), NSColor(srgbRed: 0.71, green: 0.55, blue: 0.09, alpha: 1))
var maximizeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledPlusSign, Preferences.fontIconSize, NSColor(srgbRed: 0.32, green: 0.76, blue: 0.17, alpha: 1), nil), NSColor(srgbRed: 0.04, green: 0.39, blue: 0.02, alpha: 1))
var hStackView: NSStackView!
var mouseUpCallback: (() -> Void)!
var mouseMovedCallback: (() -> Void)!
@@ -42,9 +43,16 @@ class ThumbnailView: NSStackView {
hStackView.spacing = Preferences.intraCellPadding
setViews([hStackView, thumbnail], in: .leading)
addWindowControls()
addDockLabelIcon()
}
func addWindowControls() {
private func addDockLabelIcon() {
appIcon.addSubview(dockLabelIcon, positioned: .above, relativeTo: nil)
dockLabelIcon.topAnchor.constraint(equalTo: appIcon.topAnchor, constant: -4).isActive = true
dockLabelIcon.rightAnchor.constraint(equalTo: appIcon.rightAnchor, constant: 1).isActive = true
}
private func addWindowControls() {
thumbnail.addSubview(closeIcon, positioned: .above, relativeTo: nil)
thumbnail.addSubview(minimizeIcon, positioned: .above, relativeTo: nil)
thumbnail.addSubview(maximizeIcon, positioned: .above, relativeTo: nil)
@@ -117,10 +125,19 @@ class ThumbnailView: NSStackView {
assignIfDifferent(&minimizedIcon.isHidden, !element.isMinimized || Preferences.hideStatusIcons)
assignIfDifferent(&spaceIcon.isHidden, Spaces.isSingleSpace || Preferences.hideSpaceNumberLabels)
if !spaceIcon.isHidden {
if element.isOnAllSpaces {
if element.spaceIndex > 30 || element.isOnAllSpaces {
spaceIcon.setStar()
} else {
spaceIcon.setNumber(UInt32(element.spaceIndex))
spaceIcon.setNumber(element.spaceIndex, false)
}
}
assignIfDifferent(&dockLabelIcon.isHidden, element.dockLabel == nil || Preferences.hideAppBadges || Preferences.iconSize == 0)
if !dockLabelIcon.isHidden, let dockLabel = element.dockLabel {
let view = dockLabelIcon.subviews[1] as! ThumbnailFontIconView
if dockLabel > 30 {
view.setFilledStar()
} else {
view.setNumber(dockLabel, true)
}
}
assignIfDifferent(&frame.size.width, max(thumbnail.frame.size.width + Preferences.intraCellPadding * 2, ThumbnailView.widthMin(screen)))
@@ -24,6 +24,7 @@ class AppearanceTab: NSViewController, PreferencePane {
LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide status icons:", comment: ""), "hideStatusIcons"),
LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Show standard tabs as windows:", comment: ""), "showTabsAsWindows"),
LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide colored circles on mouse hover:", comment: ""), "hideColoredCircles"),
LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide app badges:", comment: ""), "hideAppBadges"),
])
grid.column(at: 0).xPlacement = .trailing
grid.rowAlignment = .lastBaseline